Skip to content

feat(ui): add pmovesui API routes and dashboard pages - #922

Merged
POWERFULMOVES merged 16 commits into
mainfrom
feat/pmovesui-api-routes
Mar 14, 2026
Merged

POWERFULMOVES merged 16 commits into
mainfrom
feat/pmovesui-api-routes

Conversation

@POWERFULMOVES

@POWERFULMOVES POWERFULMOVES commented Mar 14, 2026

Copy link
Copy Markdown
Owner

Summary

Adds new API routes and dashboard pages for the pmovesui dashboard:

  • Agent Taxonomy API (/api/agents/taxonomy) - Returns PMOVES agent classification with type/class/evolution filters
  • GitHub PR API (/api/github/prs) - Fetches PR status with draft state tracking
  • Graphiti Trails API (/api/graphiti/trails) - Returns CHIT-signed trail entries
  • Enhanced Health API (/api/services/health-enhanced) - Extended health checks with tier statistics

TypeScript Types

Adds type definitions for:

  • PRStatus with isDraft property
  • AgentTaxonomyResponse with AgentType/AgentClass enums
  • GraphitiTrailsResponse with CHIT signature verification

Dependencies

This PR is independent and can be merged separately from #906.

Testing

  • TypeScript compilation: ✅ passes
  • ESLint: ✅ passes

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

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Agent Taxonomy dashboard with filtering and search capabilities
    • Added GitHub PR dashboard displaying pull request data and metrics
    • Added Graphiti trails dashboard with verification and filtering
    • Introduced Agent Zero, Archon, and Flute API clients for expanded integrations
    • Added comprehensive health check API with database connectivity validation
    • Implemented rate limiting on health presign endpoint
    • Added resilience layer with retry and circuit breaker capabilities
  • Improvements

    • Enhanced error responses with structured messaging and diagnostic details
    • Simplified Jellyfin dashboard interface
    • Optimized research dashboard with polling and improved state management
    • Added GitHub Actions workflow for automated UI testing

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@POWERFULMOVES has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 21 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11a25d6e-bf90-4edd-b318-74b6adf82c63

📥 Commits

Reviewing files that changed from the base of the PR and between ea7c4fc and e106f3d.

📒 Files selected for processing (68)
  • .github/workflows/ui-tests.yml
  • pmoves/ui/__tests__/serviceHealth.test.ts
  • pmoves/ui/__tests__/utils/test-helpers.ts
  • pmoves/ui/app/api/agents/taxonomy/route.ts
  • pmoves/ui/app/api/chat/messages/route.ts
  • pmoves/ui/app/api/github/prs/route.ts
  • pmoves/ui/app/api/graphiti/trails/route.ts
  • pmoves/ui/app/api/health-all/route.ts
  • pmoves/ui/app/api/health/boot-jwt/route.ts
  • pmoves/ui/app/api/health/ingest-smoke/route.ts
  • pmoves/ui/app/api/health/presign/route.ts
  • pmoves/ui/app/api/health/route.ts
  • pmoves/ui/app/api/services-hub/route.ts
  • pmoves/ui/app/api/services/health-enhanced/route.ts
  • pmoves/ui/app/api/uploads/persist/route.ts
  • pmoves/ui/app/api/uploads/presign/route.ts
  • pmoves/ui/app/dashboard/agents/page.tsx
  • pmoves/ui/app/dashboard/github/page.tsx
  • pmoves/ui/app/dashboard/graphiti/page.tsx
  • pmoves/ui/app/dashboard/ingestion-queue/page.tsx
  • pmoves/ui/app/dashboard/jellyfin/page.tsx
  • pmoves/ui/app/dashboard/research/page.tsx
  • pmoves/ui/app/dashboard/search/page.tsx
  • pmoves/ui/app/dashboard/services/page.tsx
  • pmoves/ui/components/DashboardNavigation.tsx
  • pmoves/ui/components/hub/SystemStatsBar.tsx
  • pmoves/ui/components/ingestion/ApprovalRulesConfig.test.tsx
  • pmoves/ui/components/ingestion/ApprovalRulesConfig.tsx
  • pmoves/ui/components/ingestion/BulkApprovalActions.test.tsx
  • pmoves/ui/components/ingestion/BulkApprovalActions.tsx
  • pmoves/ui/components/jellyfin/BackfillControls.tsx
  • pmoves/ui/components/jellyfin/JellyfinMediaBrowser.tsx
  • pmoves/ui/components/jellyfin/SyncStatus.test.tsx
  • pmoves/ui/components/jellyfin/SyncStatus.tsx
  • pmoves/ui/components/research/ResearchResults.tsx
  • pmoves/ui/components/research/ResearchTaskList.test.tsx
  • pmoves/ui/components/search/SearchBar.test.tsx
  • pmoves/ui/components/search/SearchBar.tsx
  • pmoves/ui/components/search/SearchFilters.test.tsx
  • pmoves/ui/components/services/TierOverview.tsx
  • pmoves/ui/components/tensorzero/FunctionEditor.tsx
  • pmoves/ui/components/tensorzero/SmartDefaultsSelector.tsx
  • pmoves/ui/components/tensorzero/api.ts
  • pmoves/ui/components/tensorzero/hooks.ts
  • pmoves/ui/components/tensorzero/smart-defaults.ts
  • pmoves/ui/components/tokenism/GeometricView.tsx
  • pmoves/ui/components/tokenism/ResultsPanel.tsx
  • pmoves/ui/e2e/archon-prompts.spec.ts
  • pmoves/ui/e2e/chat.spec.ts
  • pmoves/ui/e2e/ingestion.spec.ts
  • pmoves/ui/e2e/jellyfin.spec.ts
  • pmoves/ui/e2e/research.spec.ts
  • pmoves/ui/e2e/search.spec.ts
  • pmoves/ui/e2e/services-health.spec.ts
  • pmoves/ui/lib/api/agent-zero.ts
  • pmoves/ui/lib/api/archon.test.ts
  • pmoves/ui/lib/api/archon.ts
  • pmoves/ui/lib/api/flute.test.ts
  • pmoves/ui/lib/api/flute.ts
  • pmoves/ui/lib/api/hirag.test.ts
  • pmoves/ui/lib/api/index.ts
  • pmoves/ui/lib/constants/errorIds.ts
  • pmoves/ui/lib/fluteClient.ts
  • pmoves/ui/lib/resilience.ts
  • pmoves/ui/lib/serviceDiscovery.ts
  • pmoves/ui/lib/types/agents.ts
  • pmoves/ui/lib/types/github.ts
  • pmoves/ui/lib/types/graphiti.ts
📝 Walkthrough

Walkthrough

Introduces a comprehensive enhancement to the PMOVES UI including new API routes for agent taxonomy, GitHub PR data, Graphiti trails, and enhanced health checks; adds new dashboard pages for agents, GitHub PRs, and Graphiti trails; implements new API client libraries for Agent Zero, Archon, and Flute; adds resilience mechanisms with retry and circuit breaker patterns; and refactors multiple tests and components for improved code quality.

Changes

Cohort / File(s) Summary
GitHub Actions CI/CD
.github/workflows/ui-tests.yml
Introduces comprehensive UI test workflow with lint, typecheck, unit, e2e, and integration test jobs, including Playwright setup, Codecov coverage uploads, and hardened runners.
API Routes - Taxonomy & Metadata
pmoves/ui/app/api/agents/taxonomy/route.ts, pmoves/ui/lib/types/agents.ts, pmoves/ui/lib/types/github.ts, pmoves/ui/lib/types/graphiti.ts
Adds new API endpoint for agent taxonomy with comprehensive parsing, filtering, and aggregation; introduces strongly-typed data models for agents, GitHub PRs, and Graphiti trails.
API Routes - Data Integration
pmoves/ui/app/api/github/prs/route.ts, pmoves/ui/app/api/graphiti/trails/route.ts
Implements GitHub PR and Graphiti trail data aggregation endpoints with filtering, sorting, and caching headers; supports token resolution and structured error handling.
API Routes - Health & Monitoring
pmoves/ui/app/api/health/route.ts, pmoves/ui/app/api/health-all/route.ts, pmoves/ui/app/api/health/boot-jwt/route.ts, pmoves/ui/app/api/health/ingest-smoke/route.ts, pmoves/ui/app/api/health/presign/route.ts, pmoves/ui/app/api/services-hub/route.ts, pmoves/ui/app/api/services/health-enhanced/route.ts
Enhances health check endpoints with database connectivity checks, rate limiting, improved error responses, and per-tier service aggregation; changes runtime from edge to nodejs for DNS resolution.
API Routes - Uploads
pmoves/ui/app/api/uploads/persist/route.ts, pmoves/ui/app/api/uploads/presign/route.ts, pmoves/ui/app/api/chat/messages/route.ts
Minor parameter naming changes (unused parameters prefixed with underscore) with no functional impact.
API Clients - New Services
pmoves/ui/lib/api/agent-zero.ts, pmoves/ui/lib/api/archon.ts, pmoves/ui/lib/api/flute.ts
Introduces comprehensive API client libraries for Agent Zero orchestrator, Archon prompt service, and Flute voice synthesis with timeout handling, structured error logging, and type-safe models.
API Clients - Existing & Tests
pmoves/ui/lib/api/archon.test.ts, pmoves/ui/lib/api/flute.test.ts, pmoves/ui/lib/api/hirag.test.ts, pmoves/ui/lib/api/index.ts
Adds extensive integration tests for Archon and Flute clients; reorganizes API exports by grouping; exports cache-clearing utility for testing.
Infrastructure & Resilience
pmoves/ui/lib/resilience.ts, pmoves/ui/lib/serviceDiscovery.ts, pmoves/ui/lib/constants/errorIds.ts, pmoves/ui/lib/fluteClient.ts
Implements retry and circuit breaker patterns for resilient API calls; unifies cross-platform environment variable resolution; extends error ID catalog; minor variable renaming.
Dashboard Pages - New
pmoves/ui/app/dashboard/agents/page.tsx, pmoves/ui/app/dashboard/github/page.tsx, pmoves/ui/app/dashboard/graphiti/page.tsx
Introduces three new client-side dashboard pages with data fetching, filtering, search, and view-toggle capabilities; includes localized components for tree/grid views and data display.
Dashboard Pages - Updated
pmoves/ui/app/dashboard/jellyfin/page.tsx, pmoves/ui/app/dashboard/research/page.tsx, pmoves/ui/app/dashboard/search/page.tsx, pmoves/ui/app/dashboard/services/page.tsx, pmoves/ui/app/dashboard/ingestion-queue/page.tsx
Refactors Jellyfin page to streamline layout; updates Research page polling logic; removes unused imports and props; simplifies Search and Services dashboard logic.
UI Components - Service Health & Overview
pmoves/ui/components/services/TierOverview.tsx, pmoves/ui/components/hub/SystemStatsBar.tsx
Removes healthMap prop propagation; simplifies prop interfaces and type definitions.
UI Components - Ingestion
pmoves/ui/components/ingestion/ApprovalRulesConfig.tsx, pmoves/ui/components/ingestion/ApprovalRulesConfig.test.tsx, pmoves/ui/components/ingestion/BulkApprovalActions.tsx, pmoves/ui/components/ingestion/BulkApprovalActions.test.tsx
Removes useCallback import, unused modal classes, and props (statusFilter, sourceFilter); removes unused imports and simplifies test variable handling.
UI Components - Jellyfin
pmoves/ui/components/jellyfin/BackfillControls.tsx, pmoves/ui/components/jellyfin/JellyfinMediaBrowser.tsx, pmoves/ui/components/jellyfin/SyncStatus.tsx, pmoves/ui/components/jellyfin/SyncStatus.test.tsx
Removes unused button style constants and state management; adds ESLint suppression for native img; removes unnecessary imports and internal state.
UI Components - Search & Research
pmoves/ui/components/search/SearchBar.tsx, pmoves/ui/components/search/SearchBar.test.tsx, pmoves/ui/components/search/SearchFilters.test.tsx, pmoves/ui/components/research/ResearchTaskList.test.tsx, pmoves/ui/components/research/ResearchResults.tsx
Refactors SearchBar state initialization to lazy-load from localStorage; removes unused imports and test variables; simplifies test assertions.
UI Components - TensorZero
pmoves/ui/components/tensorzero/FunctionEditor.tsx, pmoves/ui/components/tensorzero/SmartDefaultsSelector.tsx, pmoves/ui/components/tensorzero/api.ts, pmoves/ui/components/tensorzero/hooks.ts, pmoves/ui/components/tensorzero/smart-defaults.ts
Removes unused imports and props (currentConfig); simplifies function signatures; reduces import surface for test types.
UI Components - Tokenism
pmoves/ui/components/tokenism/GeometricView.tsx, pmoves/ui/components/tokenism/ResultsPanel.tsx
Renames unused error state variable; removes unused import; adds effect early-return optimization.
Test Utilities
pmoves/ui/__tests__/utils/test-helpers.ts, pmoves/ui/__tests__/serviceHealth.test.ts
Refactors default export to named constant; renames internal parameters to underscore-prefixed form; improves TypeScript error handling in test helpers.
E2E Tests - Comprehensive Refactoring
pmoves/ui/e2e/archon-prompts.spec.ts, pmoves/ui/e2e/chat.spec.ts, pmoves/ui/e2e/ingestion.spec.ts, pmoves/ui/e2e/jellyfin.spec.ts, pmoves/ui/e2e/research.spec.ts, pmoves/ui/e2e/search.spec.ts, pmoves/ui/e2e/services-health.spec.ts
Expands test coverage from broad suites to granular, multi-scenario flows; adds filtering, search, CRUD, and state validation tests; refactors assertions and variable handling; simplifies visibility checks; introduces conditional assertions for optional UI elements.
Navigation & Type Updates
pmoves/ui/components/DashboardNavigation.tsx
Extends NavKey union to include agents, github, and graphiti dashboard entries.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser/UI
    participant AgentsTaxonomyAPI as /api/agents/taxonomy
    participant ServiceCatalog as SERVICE_CATALOG
    participant TaxonomyData as Taxonomy Files
    participant Dashboard as Agents Dashboard Page

    Browser->>+AgentsTaxonomyAPI: GET /api/agents/taxonomy?filters
    AgentsTaxonomyAPI->>+ServiceCatalog: Load catalog entries
    AgentsTaxonomyAPI->>+TaxonomyData: Parse markdown & signatures
    TaxonomyData-->>-AgentsTaxonomyAPI: Extracted types, tiers, layers
    ServiceCatalog-->>-AgentsTaxonomyAPI: Catalog entries
    AgentsTaxonomyAPI->>AgentsTaxonomyAPI: Build agents, apply filters, group by class/type/stage
    AgentsTaxonomyAPI-->>-Browser: JSON {agents, byClass, byType, byStage, total, timestamp}
    Browser->>Dashboard: Render taxonomy with filters
    Dashboard->>Dashboard: Populate tree/grid views, search, class/type filters
    Dashboard-->>Browser: Display Agent Taxonomy Dashboard
Loading
sequenceDiagram
    participant Browser as Browser/UI
    participant GitHubPRAPI as /api/github/prs
    participant Archon as Archon Service
    participant GitHubAPI as GitHub REST API
    participant Dashboard as GitHub Dashboard Page

    Browser->>+GitHubPRAPI: GET /api/github/prs?state=OPEN&limit=50
    GitHubPRAPI->>+Archon: Get GitHub token
    Archon-->>-GitHubPRAPI: Token or null
    GitHubPRAPI->>+GitHubAPI: Query PRs across repos (with token)
    GitHubAPI-->>-GitHubPRAPI: PR data array
    GitHubPRAPI->>GitHubPRAPI: Flatten, sort by updatedAt, aggregate stats
    GitHubPRAPI-->>-Browser: JSON {items: PRStatus[], counts: {open, merged, closed, draft, total}}
    Browser->>Dashboard: Auto-refresh every 30s
    Dashboard->>Dashboard: Render PR list, filters, stats bar
    Dashboard-->>Browser: Display GitHub PR Dashboard
Loading
sequenceDiagram
    participant Browser as Browser/UI
    participant GraphitiAPI as /api/graphiti/trails
    participant TrailLog as graphiti_signed_latest.json
    participant Dashboard as Graphiti Dashboard Page

    Browser->>+GraphitiAPI: GET /api/graphiti/trails?agentId=X&verifiedOnly=true
    GraphitiAPI->>+TrailLog: Read trail entries
    TrailLog-->>-GraphitiAPI: Raw trail data with signatures
    GraphitiAPI->>GraphitiAPI: Convert to TrailEntry, infer isVerified from signature, calculate stats
    GraphitiAPI->>GraphitiAPI: Filter by agentId/phase/resonance/search, sort by timestamp desc
    GraphitiAPI-->>-Browser: JSON {items: TrailEntry[], stats: TrailStats, timestamp}
    Browser->>Dashboard: Fetch with filter controls
    Dashboard->>Dashboard: Derive unique agents, render TrailCard list with verification badges
    Dashboard-->>Browser: Display Graphiti Dashboard
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, api, ui, testing, e2e-tests

Poem

🐰 A rabbit's burrow of features deep,
New dashboards dance where agents leap,
From GitHub PRs to Graphiti trails,
Where resilience flows and no call fails—
With parsed taxonomies and tested grace,
The UI dashboard finds its place!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pmovesui-api-routes

Comment on lines +49 to +56
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
signal: AbortSignal.timeout(10000),
});

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

In general, to fix this kind of issue you ensure that user input cannot arbitrarily control the URL used in server‑side requests. Either avoid using user input in the URL at all, or strictly validate it: most robustly by mapping user input to an allow‑listed set of known safe values. When dynamic selection is required, constrain and sanitize it so it cannot change the hostname and cannot target unintended resources.

For this specific code, the most compatible fix is to allow clients to filter by repository only among a predefined list of repositories (MAIN_REPOS). Instead of taking the repo query parameter verbatim, we should check whether it matches one of the allowed repository names and ignore it (or reject the request) if it doesn’t. This preserves existing functionality for the known repos while preventing the server from making GitHub API calls to arbitrary repositories within the organization.

Concretely:

  • In GET, after reading repoFilter, derive a safeRepoFilter as either a member of MAIN_REPOS or null if the param is absent/invalid.
  • Use safeRepoFilter instead of repoFilter when computing reposToQuery.
  • Optionally (but not strictly required to fix the SSRF finding), we could also add a simple format check (e.g., no slashes) before comparing, but allow‑listing already provides strong protection.

All changes are within pmoves/ui/app/api/github/prs/route.ts, in the GET handler region. No new imports or helper functions are necessary.


Suggested changeset 1
pmoves/ui/app/api/github/prs/route.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pmoves/ui/app/api/github/prs/route.ts b/pmoves/ui/app/api/github/prs/route.ts
--- a/pmoves/ui/app/api/github/prs/route.ts
+++ b/pmoves/ui/app/api/github/prs/route.ts
@@ -130,6 +130,9 @@
     ['OPEN', 'CLOSED', 'MERGED', 'DRAFT'].includes(s)
   ) as PRState[];
 
+  // Validate repo filter against allowed repositories to avoid SSRF-style misuse
+  const safeRepoFilter = repoFilter && MAIN_REPOS.includes(repoFilter) ? repoFilter : null;
+
   try {
     // Get GitHub token from Archon or use fallback
     const token = await getGitHubToken();
@@ -147,8 +150,8 @@
       );
     }
 
-    // Fetch PRs from all configured repos (or single repo if filtered)
-    const reposToQuery = repoFilter ? [repoFilter] : MAIN_REPOS;
+    // Fetch PRs from all configured repos (or single repo if filtered and allowed)
+    const reposToQuery = safeRepoFilter ? [safeRepoFilter] : MAIN_REPOS;
     const prPromises = reposToQuery.map((repo) => fetchRepoPRs(repo, token, states));
 
     const prArrays = await Promise.all(prPromises);
EOF
@@ -130,6 +130,9 @@
['OPEN', 'CLOSED', 'MERGED', 'DRAFT'].includes(s)
) as PRState[];

// Validate repo filter against allowed repositories to avoid SSRF-style misuse
const safeRepoFilter = repoFilter && MAIN_REPOS.includes(repoFilter) ? repoFilter : null;

try {
// Get GitHub token from Archon or use fallback
const token = await getGitHubToken();
@@ -147,8 +150,8 @@
);
}

// Fetch PRs from all configured repos (or single repo if filtered)
const reposToQuery = repoFilter ? [repoFilter] : MAIN_REPOS;
// Fetch PRs from all configured repos (or single repo if filtered and allowed)
const reposToQuery = safeRepoFilter ? [safeRepoFilter] : MAIN_REPOS;
const prPromises = reposToQuery.map((repo) => fetchRepoPRs(repo, token, states));

const prArrays = await Promise.all(prPromises);
Copilot is powered by AI and may make mistakes. Always verify output.
});

if (!response.ok) {
console.error(`GitHub API error for ${repo}:`, response.status, response.statusText);

Check failure

Code scanning / CodeQL

Use of externally-controlled format string High

Format string depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

In general, to fix use of externally-controlled format strings with console.*/util.format, avoid passing untrusted data in the first argument (the format string). Instead, use a constant format string (e.g. "GitHub API error for %s:") and pass untrusted data as subsequent arguments, or pre-build a safe string before calling the logging function.

For this specific case, we should stop embedding repo in the first argument to console.error. The best minimal change that preserves existing behaviour is:

  • Change the first argument to be a fixed string without any user-controlled part, for example "GitHub API error for repository:".
  • Pass repo as a separate argument so it is interpolated by console.error as data, not as part of the format string.

So line 59 should be changed from:

console.error(`GitHub API error for ${repo}:`, response.status, response.statusText);

to something like:

console.error('GitHub API error for repository:', repo, response.status, response.statusText);

This keeps the same information in the logs while removing the untrusted value from the format string position. No additional imports or helpers are required, and all changes are confined to pmoves/ui/app/api/github/prs/route.ts.

Suggested changeset 1
pmoves/ui/app/api/github/prs/route.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pmoves/ui/app/api/github/prs/route.ts b/pmoves/ui/app/api/github/prs/route.ts
--- a/pmoves/ui/app/api/github/prs/route.ts
+++ b/pmoves/ui/app/api/github/prs/route.ts
@@ -56,7 +56,7 @@
   });
 
   if (!response.ok) {
-    console.error(`GitHub API error for ${repo}:`, response.status, response.statusText);
+    console.error('GitHub API error for repository:', repo, response.status, response.statusText);
     return [];
   }
 
EOF
@@ -56,7 +56,7 @@
});

if (!response.ok) {
console.error(`GitHub API error for ${repo}:`, response.status, response.statusText);
console.error('GitHub API error for repository:', repo, response.status, response.statusText);
return [];
}

Copilot is powered by AI and may make mistakes. Always verify output.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (5)
pmoves/ui/e2e/ingestion.spec.ts (1)

101-107: ⚠️ Potential issue | 🟡 Minor

Awaited count result is discarded - this line has no effect.

await pendingItems.count(); evaluates and discards the result. If the count isn't needed, remove the line. If it's for timing/waiting purposes, add a comment explaining the intent.

🔧 Proposed fix - remove dead code
     // Only pending items should be selected
     const pendingItems = page.locator('[data-testid="queue-item"][data-status="pending"]');
-    await pendingItems.count();
 
     // Verify button exists and is clickable
     await expect(page.locator('[data-testid="select-pending"]')).toBeVisible();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/ingestion.spec.ts` around lines 101 - 107, The line awaiting
pendingItems.count() in the test is a no-op because the returned value is
discarded; either remove the dead call or replace it with an assertion or
explicit wait: for example use expect(pendingItems).toHaveCount(n) if you want
to assert a specific number, or add a comment explaining it was intended as a
timing hack and wrap it in a proper wait (e.g., waitFor/expect) instead; locate
the statement referencing the pendingItems locator and update/remove the await
pendingItems.count() accordingly.
pmoves/ui/e2e/jellyfin.spec.ts (1)

100-105: ⚠️ Potential issue | 🟡 Minor

Awaited count result is discarded - this line has no effect.

Same issue as in ingestion.spec.ts: await searchResults.count(); does nothing useful since the result is discarded.

🔧 Proposed fix
     // Check that search results are shown
     const searchResults = page.locator('[data-testid="media-item"]');
-    await searchResults.count();
 
     // Results might be filtered or empty
     await expect(page.locator('[data-testid="media-browser"]')).toBeVisible();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/jellyfin.spec.ts` around lines 100 - 105, The awaited
searchResults.count() call is a no-op because its return value is discarded;
replace it with a meaningful use such as capturing the value (const resultCount
= await searchResults.count();) and asserting it (e.g.,
expect(resultCount).toBeGreaterThanOrEqual(0)) or, if you intended to wait for
items, use Playwright's locator assertion (await
expect(searchResults).toHaveCount(expectedCount) or a >= check); update the code
around the searchResults locator in jellyfin.spec.ts accordingly.
pmoves/ui/e2e/research.spec.ts (2)

81-94: ⚠️ Potential issue | 🟠 Major

Expand/collapse test stopped asserting the collapse outcome.

Line 93 stores _isCollapsed but never verifies it, so the collapse behavior is currently untested.

Suggested fix
-    const _isCollapsed = await optionsPanel.isVisible({ timeout: 1000 }).catch(() => false);
+    await expect(optionsPanel).not.toBeVisible({ timeout: 2000 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/research.spec.ts` around lines 81 - 94, The test captures
collapse state into the variable _isCollapsed but never asserts it; add an
assertion after computing _isCollapsed to verify the panel is hidden (e.g.,
assert _isCollapsed is false or use expect(optionsPanel).not.toBeVisible()),
referencing the existing optionsPanel and _isCollapsed variables after the
collapse click (and you can remove the unused _isInitiallyVisible if desired).

239-243: ⚠️ Potential issue | 🟠 Major

select pending test has no behavioral assertion.

Line 239 only constructs a locator; it doesn’t verify that pending selection actually happened. This can pass even when selection logic is broken.

Suggested fix
-    page.locator('[data-testid="task-item"][data-status="pending"]');
+    const nonPendingChecked = page.locator(
+      '[data-testid="task-item"]:not([data-status="pending"]) [data-testid="task-checkbox"]:checked'
+    );
+    await expect(nonPendingChecked).toHaveCount(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/research.spec.ts` around lines 239 - 243, The test currently
only constructs page.locator('[data-testid="task-item"][data-status="pending"]')
and checks visibility of '[data-testid="select-pending"]' but never asserts that
selecting pending actually changes state; update the test to perform the
selection action (e.g., await page.click('[data-testid="select-pending"]') or
await page.locator('[data-testid="select-pending"]').click()) and then assert a
behavioral change such as that the pending items are moved/marked (for example
await
expect(page.locator('[data-testid="task-item"][data-status="selected"]')).toBeVisible()
or that page.locator('[data-testid="task-item"][data-status="pending"]').count()
has decreased) so the test verifies the selection logic rather than just
constructing a locator.
pmoves/ui/app/dashboard/jellyfin/page.tsx (1)

74-87: ⚠️ Potential issue | 🟠 Major

Guarantee backfilling state reset on thrown errors.

If triggerBackfill throws instead of returning { ok: false }, setBackfilling(false) is skipped and the UI can remain stuck.

Suggested fix
   const handleBackfill = async (limit = 50) => {
     setBackfilling(true);
     setError(null);

-    const result = await triggerBackfill({ limit });
-    if (result.ok) {
-      // Backfill started successfully - refresh status to see updates
-      await refreshSyncStatus();
-    } else {
-      setError(result.error);
-    }
-
-    setBackfilling(false);
+    try {
+      const result = await triggerBackfill({ limit });
+      if (result.ok) {
+        // Backfill started successfully - refresh status to see updates
+        await refreshSyncStatus();
+      } else {
+        setError(result.error);
+      }
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Backfill failed');
+    } finally {
+      setBackfilling(false);
+    }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/dashboard/jellyfin/page.tsx` around lines 74 - 87, The
handleBackfill function can throw (via triggerBackfill) and currently
setBackfilling(false) is only reached on the happy/error return path; wrap the
async work in a try/finally so setBackfilling(false) is always executed—move
setError(null) into the try, await triggerBackfill/refreshSyncStatus inside try
and handle result.error there, and call setBackfilling(false) in the finally
block; reference handleBackfill, triggerBackfill, refreshSyncStatus,
setBackfilling, and setError when making the change.
🟠 Major comments (31)
pmoves/ui/test-results/jellyfin-Jellyfin-Integration-should-browse-media-library-chromium/error-context.md-1-71 (1)

1-71: ⚠️ Potential issue | 🟠 Major

Remove generated test-result artifact from source control (or move to stable fixtures).

This looks like a transient Playwright failure snapshot under test-results/, which is typically non-deterministic and adds noisy churn to PRs. Please exclude it from committed changes unless there is an explicit policy to version these artifacts.

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

In
`@pmoves/ui/test-results/jellyfin-Jellyfin-Integration-should-browse-media-library-chromium/error-context.md`
around lines 1 - 71, Remove the transient Playwright snapshot artifact from the
commit: delete the committed test-results snapshot (the failed page snapshot
containing heading "Jellyfin Integration" [ref=e24] and refs like generic
[active] [ref=e1]) and either move a stable, intentionally versioned fixture
into your tests/fixtures directory or regenerate a deterministic fixture; also
add the test-results pattern to .gitignore (or update CI to persist artifacts
elsewhere) so future Playwright run artifacts aren’t committed.
pmoves/ui/test-results/jellyfin-Jellyfin-Integrat-474a1-ackfill-with-custom-options-chromium/error-context.md-1-71 (1)

1-71: ⚠️ Potential issue | 🟠 Major

Add test-results/ to .gitignore and remove committed test artifacts.

The pmoves/ui/test-results/ directory contains 350 auto-generated test artifacts (error snapshots, screenshots, traces) that should not be committed to version control. These files are not present in .gitignore and will cause:

  • Repository bloat from ephemeral test run outputs
  • Merge conflicts across developer branches as tests regenerate files
  • CI/CD noise on every test execution

Move any intentional test snapshots to a dedicated __snapshots__/ or tests/fixtures/ directory with stable filenames, then remove the committed test-results artifacts and add the directory to .gitignore.

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

In
`@pmoves/ui/test-results/jellyfin-Jellyfin-Integrat-474a1-ackfill-with-custom-options-chromium/error-context.md`
around lines 1 - 71, The repository contains a committed test-results directory
with many ephemeral artifacts; add "test-results/" to .gitignore, delete the
committed artifacts from the index (e.g., git rm --cached or remove and commit)
and push the cleanup, and relocate any intentional, stable snapshots into a
proper snapshots/fixtures folder such as "__snapshots__" or "tests/fixtures"
with controlled filenames; target the "test-results" artifacts shown in the diff
so CI and future commits no longer track those generated files.
pmoves/ui/test-results/ingestion-Enhanced-Video-A-ff287-create-rule-with-conditions-chromium/error-context.md-1-87 (1)

1-87: ⚠️ Potential issue | 🟠 Major

Add pmoves/ui/test-results/ to .gitignore immediately.

This directory contains 350+ ephemeral test artifacts (error snapshots, screenshots, trace files) that are being committed to the repository. These should not be version-controlled as they:

  • Are auto-generated on every test run
  • Cause unnecessary repository bloat
  • Create merge conflicts when multiple developers run tests
  • Do not serve as intentional snapshot baselines (file types and naming indicate Playwright CI artifacts)

Add the following to .gitignore:

pmoves/ui/test-results/

Then remove committed artifacts:

git rm -r --cached pmoves/ui/test-results/
git commit -m "Remove ephemeral test artifacts from version control"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@pmoves/ui/test-results/ingestion-Enhanced-Video-A-ff287-create-rule-with-conditions-chromium/error-context.md`
around lines 1 - 87, The repo currently commits ephemeral Playwright artifacts
under pmoves/ui/test-results; add a single-line entry "pmoves/ui/test-results/"
to .gitignore, then remove the tracked artifacts from Git (use git rm -r
--cached on that directory) and commit with a message like "Remove ephemeral
test artifacts from version control"; ensure CI/test runners still generate the
artifacts locally and update any developer docs if they relied on those
committed files.
pmoves/ui/test-results/ingestion-Enhanced-Video-A-f2a15--rule-against-pending-items-chromium/error-context.md-1-87 (1)

1-87: ⚠️ Potential issue | 🟠 Major

Remove pmoves/ui/test-results/ from version control and add to .gitignore.

Verification confirms that test-results artifacts (error-context.md, test-failed-*.png, trace.zip) are currently tracked in the repository. These Playwright test failure artifacts should be excluded from version control to prevent repository bloat and merge conflicts.

Actions required:

  1. Add pmoves/ui/test-results/ to .gitignore
  2. Remove tracked test-results files from git history: git rm -r --cached pmoves/ui/test-results/ followed by git commit
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@pmoves/ui/test-results/ingestion-Enhanced-Video-A-f2a15--rule-against-pending-items-chromium/error-context.md`
around lines 1 - 87, Add the test-results directory to .gitignore and remove
currently tracked test artifacts: update .gitignore to include
pmoves/ui/test-results/ (so files like error-context.md, test-failed-*.png,
trace.zip are ignored), then remove the tracked files from git with git rm -r
--cached pmoves/ui/test-results/ and commit the change so the test-results
artifacts are no longer stored in the repository.
pmoves/ui/test-results/ingestion-Enhanced-Video-A-b7d93-d-select-only-pending-items-chromium/error-context.md-1-87 (1)

1-87: ⚠️ Potential issue | 🟠 Major

Remove all test result artifacts from version control and add to .gitignore.

The test-results directory contains hundreds of generated Playwright test artifacts that should never be committed to the repository. Currently, approximately 400+ test result files (error-context.md snapshots, trace.zip files, and test failure screenshots) are tracked in git. Additionally, test-results is not listed in pmoves/ui/.gitignore, allowing these ephemeral files to be committed.

Test result artifacts must be generated locally or in CI and discarded after review—they cause repository bloat, merge conflicts across developers, and clutter git history with no lasting value.

Actions required:

  1. Remove all files in pmoves/ui/test-results/ from the repository
  2. Add test-results/ to pmoves/ui/.gitignore
  3. Configure CI to retain test artifacts separately if needed for debugging
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@pmoves/ui/test-results/ingestion-Enhanced-Video-A-b7d93-d-select-only-pending-items-chromium/error-context.md`
around lines 1 - 87, The repo contains committed Playwright artifacts in the
test-results directory (e.g., error-context.md, trace.zip, screenshots); remove
all tracked files under test-results from version control, add a test-results/
entry to pmoves/ui/.gitignore, and commit the removal and .gitignore change so
future artifacts aren’t tracked; finally, update CI to upload and retain
test-results artifacts via the CI artifacts storage (not git) for debugging.
pmoves/ui/test-results/jellyfin-Jellyfin-Integrat-cb01f-backfill-batch-size-1-1000--chromium/error-context.md-1-71 (1)

1-71: ⚠️ Potential issue | 🟠 Major

Remove test artifacts from version control and add test-results/ to .gitignore

The test-results/ directory contains auto-generated Playwright test execution artifacts (screenshots, videos, traces, error contexts) that should not be committed. Multiple test-results files are already in the repository (archon-prompts, chat, jellyfin test directories, etc.), increasing repository bloat and creating merge conflicts during parallel test runs.

Add the following pattern to .gitignore to exclude these auto-generated outputs:

test-results/
playwright-report/

Per Playwright documentation and CI best practices, these artifacts should be uploaded as build artifacts rather than committed to version control.

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

In
`@pmoves/ui/test-results/jellyfin-Jellyfin-Integrat-cb01f-backfill-batch-size-1-1000--chromium/error-context.md`
around lines 1 - 71, The repo contains committed Playwright artifacts (e.g.,
entries under test-results/ like pmoves/ui/test-results/jellyfin-...) that
should be removed and ignored; update .gitignore to add the patterns
"test-results/" and "playwright-report/", remove the committed artifacts from
version control (git rm --cached or delete and commit removal) so they are no
longer tracked, and ensure CI uploads Playwright outputs as build artifacts
instead of committing them.
pmoves/ui/app/api/services/health-enhanced/route.ts-10-11 (1)

10-11: ⚠️ Potential issue | 🟠 Major

Switch health-enhanced route to nodejs runtime for Docker DNS support.

This route calls checkAllServices(), the same function used by health-all, which was explicitly switched to nodejs with the comment: "Edge runtime doesn't support localhost DNS resolution, which is needed for service health checks within the Docker network." The health-enhanced route should also use nodejs runtime to ensure reliable DNS resolution when probing services in the Docker network. Change line 10 to export const runtime = 'nodejs';.

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

In `@pmoves/ui/app/api/services/health-enhanced/route.ts` around lines 10 - 11,
The route currently exports runtime = 'edge' which prevents localhost DNS
resolution needed by the service probes; change the runtime export in route.ts
for the health-enhanced handler from 'edge' to 'nodejs' so checkAllServices()
(same function used by health-all) can resolve Docker network hostnames
reliably; update the line exporting runtime (export const runtime = ...) to
'nodejs' and leave export const dynamic = 'force-dynamic' unchanged.
pmoves/ui/e2e/search.spec.ts-176-176 (1)

176-176: ⚠️ Potential issue | 🟠 Major

Replace swallowed visibility checks with explicit assertions

At lines 176, 288, 329, 342, and 351, isVisible().catch(() => false) is called via void, discarding both the result and any errors. These tests currently pass without verifying the visibility conditions they claim to validate.

Replace each instance with an explicit assertion, such as await expect(locator).toBeVisible(), or use explicit branching with assertions on each allowed outcome. Currently, tests can pass regardless of whether the expected elements are actually visible or hidden.

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

In `@pmoves/ui/e2e/search.spec.ts` at line 176, Several tests currently call
.isVisible().catch(() => false) and discard the result (e.g.,
page.locator('[data-testid="result-content"]').isVisible()), so visibility
checks are swallowed; find every occurrence of .isVisible().catch(() => false)
in search.spec.ts (including the locator '[data-testid="result-content"]') and
replace each with an explicit Playwright assertion—e.g., await
expect(page.locator('<same selector>')).toBeVisible() or await
expect(...).toBeHidden() as appropriate for the test case—so the test actually
verifies visibility (or explicitly branch and assert both allowed outcomes)
instead of ignoring errors/results.
pmoves/ui/e2e/search.spec.ts-71-72 (1)

71-72: ⚠️ Potential issue | 🟠 Major

No-op count check weakens test reliability

Line 72 executes count() but discards the result without awaiting or asserting it, so nothing is actually validated. This pattern repeats throughout the file (lines 176, 288, 329, 342, 351) with .isVisible() and other async operations—all voided without assertions. Tests can pass even if UI elements don't exist.

Capture the result and assert the precondition explicitly:

Proposed fix
-    // Get initial result count (result discarded, just checking elements exist)
-    void page.locator('[data-testid="search-result-item"]').count();
+    // Verify initial result count exists
+    const initialCount = await page.locator('[data-testid="search-result-item"]').count();
+    expect(initialCount).toBeGreaterThan(0);

Apply the same pattern to lines 176, 288, 329, 342, and 351—replace voided async calls with explicit awaits and assertions.

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

In `@pmoves/ui/e2e/search.spec.ts` around lines 71 - 72, The test currently calls
async helpers like page.locator('[data-testid="search-result-item"]').count()
and elementHandle.isVisible() with a leading void so the promise is neither
awaited nor asserted; replace each voided call (e.g., the initial count at the
start and the other occurrences around lines referenced) with an awaited call
and an explicit assertion (for example, const count = await
page.locator('[data-testid="search-result-item"]').count();
expect(count).toBeGreaterThan(0) or await expect(locator).toBeVisible()) so the
preconditions are actually validated; update all occurrences of void ...
.count() and void ... .isVisible() to use await plus appropriate expect
assertions in the test functions (use the same locator strings and method names
to find and change the calls).
pmoves/ui/test-results/chat-Agent-Zero-Chat-clears-input-after-sending-chromium/error-context.md-1-97 (1)

1-97: ⚠️ Potential issue | 🟠 Major

Add test artifacts to .gitignore.

This entire test-results/ directory is being tracked in version control. Playwright test artifacts (error snapshots, screenshots, traces) should be gitignored as they're auto-generated, environment-specific, and bloat the repository. Add pmoves/ui/test-results/ and related patterns to .gitignore.

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

In
`@pmoves/ui/test-results/chat-Agent-Zero-Chat-clears-input-after-sending-chromium/error-context.md`
around lines 1 - 97, The repo is tracking auto-generated Playwright artifacts
under the test-results directory (seen here as error-context.md in
test-results); update .gitignore to add the test-results directory and common
Playwright artifact patterns (e.g., test-results/, *.png, *.zip, trace/,
playwright-report/, snapshots) so these files are not committed, then remove
existing tracked artifacts from Git with git rm --cached (e.g., git rm -r
--cached test-results) and commit the .gitignore change and removal; reference
the test-results/error-context.md snapshot to verify the ignored patterns cover
the files shown.
pmoves/ui/test-results/.last-run.json-1-122 (1)

1-122: ⚠️ Potential issue | 🟠 Major

Do not commit transient test-run failure artifacts.

This file is generated run state (status: failed) and should not live in source control. It introduces churn and stale failure metadata in PRs.

🧹 Suggested cleanup
- pmoves/ui/test-results/.last-run.json
# .gitignore
+pmoves/ui/test-results/.last-run.json
+pmoves/ui/test-results/**/error-context.md
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/test-results/.last-run.json` around lines 1 - 122, The committed
JSON artifact containing the generated test-run state (it has keys like
"status":"failed" and "failedTests") is transient and should be removed from
source control; delete the file from the repo (stop tracking it with git rm
--cached <file> and commit the removal), add an appropriate ignore rule to
.gitignore to exclude these test-run artifacts (e.g. ignore the generated
.last-run.json or the test-results/*.json pattern), and push the commit so
future runs don't reintroduce stale failure metadata.
pmoves/ui/e2e/chat.spec.ts-153-166 (1)

153-166: ⚠️ Potential issue | 🟠 Major

Clear-history test lacks a post-condition assertion.

After clicking clear/confirm, there’s no verification that history/messages were actually cleared, so the test can pass on a broken implementation.

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

In `@pmoves/ui/e2e/chat.spec.ts` around lines 153 - 166, The test "allows clearing
chat history" currently clicks the clear/confirm buttons (using clearButton,
hasConfirm, page.getByRole) but never asserts the outcome; update the test to
verify the chat was actually cleared by asserting the message container (e.g.,
the selector that returns chat messages or the messages list element) contains
zero items or shows the expected empty-state text after the clear action; locate
the chat message list used elsewhere in the spec (or use
page.getByRole/getByTestId for messages) and add a wait-for/expect that message
count is 0 or that the empty-state element is visible to make the post-condition
explicit.
pmoves/ui/e2e/chat.spec.ts-125-135 (1)

125-135: ⚠️ Potential issue | 🟠 Major

Error-handling test does not trigger an error path.

This test only checks whether an error container exists and maybe starts hidden; it never forces a failed request, so it does not validate the failure UX.

Suggested fix
 test('shows error message on failed request', async ({ page }) => {
-  // This test requires mocking a failed request
-  // For now, we'll check that error handling UI exists
-  const hasErrorDisplay =
-    (await page.locator('[class*="error"], [role="alert"]').count()) > 0;
-
-  // If error display exists, verify it's hidden initially
-  if (hasErrorDisplay) {
-    await expect(page.locator('[class*="error"], [role="alert"]').first()).not.toBeVisible();
-  }
+  await page.route('**/api/chat/**', async (route) => {
+    await route.fulfill({ status: 500, body: JSON.stringify({ error: 'boom' }) });
+  });
+  await page.getByPlaceholder(/message/i).fill('trigger error');
+  await page.getByRole('button', { name: /send/i }).click();
+  await expect(page.getByRole('alert')).toBeVisible();
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/chat.spec.ts` around lines 125 - 135, The test "shows error
message on failed request" currently never triggers the error path; update it to
mock a failing network response (use Playwright's page.route to intercept the
relevant API endpoint and respond with a 500 or abort) before performing the
action that issues the request (e.g., trigger the send button or form submission
used in this spec), then assert that the error UI (locator '[class*="error"],
[role="alert"]') becomes visible; locate the intercept target by the same
network path used in the app and use route.fulfill/route.abort, then await the
user action and expect the error locator toBeVisible.
pmoves/ui/e2e/research.spec.ts-24-25 (1)

24-25: ⚠️ Potential issue | 🟠 Major

Line 24 is a no-op async check—the call is neither awaited nor assigned.

The isVisible().catch(() => false) call has no effect on test execution. Compare this to all other similar patterns in the file (lines 81, 93, 183, 286, 348+) which properly await and assign the result to a variable.

Suggested fix
-    page.locator('[data-testid="research-results"]').isVisible().catch(() => false);
+    const hasResultsSection = await page.locator('[data-testid="research-results"]').isVisible().catch(() => false);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/research.spec.ts` around lines 24 - 25, The call to
page.locator('[data-testid="research-results"]').isVisible().catch(() => false)
is a no-op because it's neither awaited nor assigned; update the test to await
the promise and capture the boolean like the other checks (e.g., assign to a
variable such as resultsVisible) so the visibility result is actually used,
matching the pattern used at lines 81/93/183/etc.; ensure you use await with the
same .catch(() => false) fallback.
pmoves/ui/e2e/chat.spec.ts-50-64 (1)

50-64: ⚠️ Potential issue | 🟠 Major

Markdown test is non-deterministic and can pass without validating markdown rendering.

Using waitForTimeout(2000) with a conditional assertion (if (hasListItems)) means the test succeeds whether or not markdown actually renders. This pattern is systemic across the file—error handling and history-clearing tests use similar soft assertions that pass without proving feature behavior.

Replace fixed sleeps with proper Playwright wait conditions (e.g., page.waitForFunction(), page.waitForSelector()) and convert conditional assertions to hard expectations that validate required behavior. E2E tests should assert that features work, not optionally validate them.

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

In `@pmoves/ui/e2e/chat.spec.ts` around lines 50 - 64, The test "supports markdown
in responses" is non-deterministic because it uses a fixed sleep
(page.waitForTimeout) and a soft conditional check (hasListItems + if) which
allows the test to pass without verifying markdown rendering; update the test to
wait explicitly for rendered markdown using deterministic Playwright waits
(e.g., replace page.waitForTimeout(2000) with page.waitForSelector('li, ul, ol')
or page.waitForFunction(...) that asserts list elements appear) and remove the
conditional branch so the assertion becomes a hard expectation (use
expect(page.locator('li').first()).toBeVisible() after the explicit wait),
ensuring the test fails when markdown is not rendered.
pmoves/ui/app/dashboard/jellyfin/page.tsx-147-152 (1)

147-152: ⚠️ Potential issue | 🟠 Major

“Link” action is a no-op in production UI.

Line [148] only logs to console, so the button presents a broken CTA to users.

Suggested short-term fix (avoid broken affordance)
-                  <button
-                    onClick={() => console.log("Link item:", item.id)}
-                    className="rounded bg-green-600 px-3 py-1 text-sm text-white hover:bg-green-700 transition"
-                  >
-                    Link
-                  </button>
+                  <button
+                    type="button"
+                    disabled
+                    className="rounded bg-neutral-400 px-3 py-1 text-sm text-white cursor-not-allowed"
+                    title="Link action is not implemented yet"
+                  >
+                    Link (coming soon)
+                  </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/dashboard/jellyfin/page.tsx` around lines 147 - 152, The "Link"
button currently only logs to console (onClick={() => console.log("Link item:",
item.id)}), creating a broken CTA; for a short-term fix replace that no-op by
disabling the control: remove the console.log handler (or leave it out), add the
disabled attribute and aria-disabled="true", update the button text/title to
"Link (coming soon)" and change classes from interactive styles to disabled
styles (e.g. remove hover/bg-green and use a muted/bg-gray and
cursor-not-allowed) so the button is non-clickable and conveys it's not
available; alternatively, if a real handler exists (e.g., linkItem or
handleLink), wire the button to that function instead of console.log.
pmoves/ui/app/api/health/route.ts-43-47 (1)

43-47: ⚠️ Potential issue | 🟠 Major

Avoid exposing raw DB error details in health responses.

checks.database.error currently returns underlying exception text. That can leak internals (schema/network/provider details) to unauthenticated callers.

Suggested fix
     checks: {
       database: {
         status: dbStatus,
-        error: dbError,
+        error: dbStatus === 'healthy'
+          ? undefined
+          : 'Database connectivity check failed',
       },
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/health/route.ts` around lines 43 - 47, The health route
currently returns raw DB error text via checks.database.error (variables
dbStatus and dbError); change this to avoid exposing internals by replacing the
returned error with a non-sensitive value (e.g., null or a generic string like
"unavailable" or "database error") while still logging the full dbError
server-side (use your logger inside the same handler where dbError is caught).
Update the code that constructs the health response (the checks/database object
in the route handler) to use the sanitized error value instead of dbError and
keep detailed dbError only in internal logs.
pmoves/ui/app/api/github/prs/route.ts-93-111 (1)

93-111: ⚠️ Potential issue | 🟠 Major

Hardcoded localhost for Archon service won't work in Edge runtime or production.

The Edge runtime executes on Vercel's edge network where localhost:8091 is not accessible. This will cause getGitHubToken() to always return null in production, resulting in 503 responses.

🔧 Proposed fix: use environment variable
+const ARCHON_URL = process.env.ARCHON_SERVICE_URL || 'http://localhost:8091';
+
 async function getGitHubToken(): Promise<string | null> {
   try {
-    const archonResponse = await fetch('http://localhost:8091/api/github/token', {
+    const archonResponse = await fetch(`${ARCHON_URL}/api/github/token`, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       signal: AbortSignal.timeout(5000),
     });

Alternatively, consider switching from Edge to Node.js runtime (runtime = 'nodejs') if the Archon service is only reachable from server infrastructure.

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

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 93 - 111, getGitHubToken
currently calls http://localhost:8091 which fails in Edge/runtime and
production; change it to read the Archon base URL from an environment variable
(e.g., process.env.ARCHON_URL or NEXT_PUBLIC_ARCHON_URL depending on runtime)
and build the fetch URL from that value instead of hardcoding localhost, keep
the POST, headers and AbortSignal.timeout behavior and handle missing env by
returning null or falling back, and ensure this change is applied inside the
getGitHubToken function so the fetch uses the configurable base URL.
pmoves/ui/app/api/github/prs/route.ts-79-81 (1)

79-81: ⚠️ Potential issue | 🟠 Major

additions, deletions, changedFiles are not returned by the PR list endpoint."GitHub's API returns different amounts of information about prs based upon how that information is retrieved." The list endpoint (/pulls) returns a ShortPullRequest which does not include additions, deletions, or changed_files fields. These are only available when fetching a specific PR via pull_request().

These fields will be undefined in the response, causing the UI to display "undefined" or NaN values.

🔧 Proposed fix: provide defaults or fetch individual PRs
   return data.map((pr: any) => ({
     number: pr.number,
     title: pr.title,
     state: mapState(pr.state),
     isDraft: pr.draft,
     author: pr.user.login,
     avatarUrl: pr.user.avatar_url,
     createdAt: pr.created_at,
     updatedAt: pr.updated_at,
     closedAt: pr.closed_at,
     mergedAt: pr.merged_at,
     url: pr.html_url,
     baseBranch: pr.base.ref,
     headBranch: pr.head.ref,
-    additions: pr.additions,
-    deletions: pr.deletions,
-    changedFiles: pr.changed_files,
+    additions: pr.additions ?? 0,
+    deletions: pr.deletions ?? 0,
+    changedFiles: pr.changed_files ?? 0,
     commentCount: pr.comments || 0,
     reviewCount: pr.review_comments || 0,

Note: This provides defaults but the values will always be 0. For accurate stats, you'd need to fetch each PR individually or use the GraphQL API which can return these fields in a single request.

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

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 79 - 81, The PR list
mapping is assigning additions, deletions, and changedFiles from a
ShortPullRequest (pr) which doesn't include those fields, so they end up
undefined; update the mapping where you set additions/deletions/changedFiles to
provide safe defaults (e.g., additions: pr.additions ?? 0, deletions:
pr.deletions ?? 0, changedFiles: pr.changed_files ?? 0) to avoid "undefined"/NaN
in the UI, or if you need accurate numbers replace the list fetch with per-PR
fetches (use octokit.rest.pulls.get or the pull_request() helper for each
pr.number with Promise.all and then merge the detailed fields into your mapped
object).
pmoves/ui/lib/api/agent-zero.ts-141-157 (1)

141-157: ⚠️ Potential issue | 🟠 Major

Timeout handles should be cleared in finally to avoid timer leaks on thrown fetch paths.

If fetch throws before clearTimeout, the timer remains scheduled. This pattern appears in multiple methods.

🧹 Proposed fix pattern
-    const response = await fetch(`${getAgentZeroUrl()}/mcp/command`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify({
-        command,
-        params,
-      }),
-      signal: controller.signal,
-    });
-
-    clearTimeout(timeoutId);
+    let response: Response;
+    try {
+      response = await fetch(`${getAgentZeroUrl()}/mcp/command`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          command,
+          params,
+        }),
+        signal: controller.signal,
+      });
+    } finally {
+      clearTimeout(timeoutId);
+    }

Also applies to: 248-264, 350-363

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

In `@pmoves/ui/lib/api/agent-zero.ts` around lines 141 - 157, The
AbortController/timeout pattern creates a timer that isn't cleared if fetch
throws; wrap the fetch call in try...finally and move clearTimeout(timeoutId)
into the finally block so the timer is always cleared (i.e., create controller
and timeoutId as shown, then try { const response = await fetch(..., signal:
controller.signal); ... } finally { clearTimeout(timeoutId); }). Apply the same
change to the other occurrences of this pattern (the blocks that use
AbortController, timeoutId, clearTimeout(timeoutId), AGENT_ZERO_MCP_TIMEOUT and
fetch(getAgentZeroUrl()...)) to avoid timer leaks.
pmoves/ui/e2e/archon-prompts.spec.ts-74-75 (1)

74-75: ⚠️ Potential issue | 🟠 Major

These assertions are tautologies and always pass.

expect(expr || true).toBe(true) cannot fail, so category/search/save verification is effectively disabled.

✅ Proposed fix
-      expect(hasCategoryInUrl || true).toBe(true); // Soft assertion
+      expect(hasCategoryInUrl).toBe(true);
@@
-      expect(url.includes('search') || true).toBe(true);
+      expect(url.includes('search')).toBe(true);
@@
-        expect(hasSuccess || true).toBe(true);
+        expect(hasSuccess).toBe(true);

Also applies to: 91-92, 195-195

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

In `@pmoves/ui/e2e/archon-prompts.spec.ts` around lines 74 - 75, The tests contain
tautological assertions like expect(hasCategoryInUrl || true).toBe(true) which
always pass; locate the failing assertions that use the pattern "|| true" (e.g.,
the variable hasCategoryInUrl and the similar hasSearchInUrl / hasSavedInUrl
uses) in archon-prompts.spec.ts and remove the "|| true" so the assertion checks
the real boolean (e.g., expect(hasCategoryInUrl).toBe(true) or
expect(hasCategoryInUrl).toBeTruthy()); update all occurrences (the other two
similar assertions reported) to assert the actual condition instead of OR-ing
with true.
pmoves/ui/lib/resilience.ts-336-346 (1)

336-346: ⚠️ Potential issue | 🟠 Major

Server 5xx responses currently bypass both retry and circuit-breaker failure tracking.

fetch resolves on HTTP errors, so non-OK responses are returned as successes. That means transient 5xx responses neither retry nor contribute to circuit opening.

🛡️ Proposed fix
 export async function resilientFetch(
@@
 ): Promise<Response> {
   const circuitBreaker =
     options?.circuitBreaker?.breaker ||
     new CircuitBreaker(options?.circuitBreaker);
+  const baseRetryable = options?.retry?.retryable ?? DEFAULT_RETRY_OPTIONS.retryable;
+  const retryOptions: RetryOptions = {
+    ...options?.retry,
+    retryable: (error) => {
+      if (error instanceof Error && /^HTTP 5\d\d$/.test(error.message)) return true;
+      return baseRetryable(error);
+    },
+  };

   return circuitBreaker.execute(() =>
     retry(
-      () =>
-        fetch(url, {
+      async () => {
+        const response = await fetch(url, {
           ...init,
           // Add timeout via AbortSignal if not provided
           signal: init?.signal || AbortSignal.timeout(30000),
-        }),
-      options?.retry
+        });
+        if (response.status >= 500) {
+          throw new Error(`HTTP ${response.status}`);
+        }
+        return response;
+      },
+      retryOptions
     )
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/lib/resilience.ts` around lines 336 - 346, The current fetch call
inside circuitBreaker.execute(...) returns HTTP error responses as successes, so
update the wrapped operation used by retry and circuit breaker (the arrow
function passed into retry within resilience.ts) to inspect the fetch Response
and throw a descriptive error when response.ok is false (include status and
statusText or response body snippet) so non-2xx/3xx responses trigger retry and
count as failures for circuitBreaker.execute; preserve the existing
AbortSignal/timeout behavior and only return the Response when response.ok is
true.
pmoves/ui/lib/api/archon.ts-459-473 (1)

459-473: ⚠️ Potential issue | 🟠 Major

Clear execution timeout in finally to avoid pending timers on failure paths.

If the request throws before Line 472, the timeout is never cleared and remains queued.

🧹 Proposed fix
-    const response = await fetch(`${getArchonUrl()}/api/prompts/execute`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify(request),
-      signal: controller.signal,
-    });
-
-    clearTimeout(timeoutId);
+    let response: Response;
+    try {
+      response = await fetch(`${getArchonUrl()}/api/prompts/execute`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(request),
+        signal: controller.signal,
+      });
+    } finally {
+      clearTimeout(timeoutId);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/lib/api/archon.ts` around lines 459 - 473, The timeout created with
setTimeout (timeoutId) can remain pending if the fetch throws; wrap the fetch
call (the await fetch(`${getArchonUrl()}/api/prompts/execute`, ... ) that uses
controller.signal) in a try/finally and move clearTimeout(timeoutId) into the
finally block so the timer is always cleared regardless of errors, keeping
controller and timeoutId in scope; optionally still call controller.abort() only
where appropriate but ensure clearTimeout(timeoutId) runs in the finally.
pmoves/ui/app/dashboard/agents/page.tsx-52-56 (1)

52-56: ⚠️ Potential issue | 🟠 Major

Use a semantic button for class filter cards.

This control is clickable but rendered as a div, which breaks keyboard accessibility. It should be a button (with pressed state).

♿ Proposed fix
-              <div
+              <button
+                type="button"
                 key={cls}
                 className="card-glass p-4 cursor-pointer hover:border-cata-cyan transition-colors"
                 onClick={() => setClassFilter(classFilter === cls ? "ALL" : cls)}
+                aria-pressed={classFilter === cls}
               >
                 <span className="text-xs text-ink-muted uppercase tracking-wider">{cls}</span>
                 <div className="font-display font-bold text-2xl">
                   {data.byClass[cls]?.length || 0}
                 </div>
-              </div>
+              </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/dashboard/agents/page.tsx` around lines 52 - 56, Replace the
clickable div used for class filter cards with a semantic button element so it
is keyboard-accessible and can expose pressed state; update the element that
currently uses onClick={() => setClassFilter(classFilter === cls ? "ALL" : cls)}
to a <button> (keep the same onClick handler and className "card-glass p-4
cursor-pointer hover:border-cata-cyan transition-colors"), add aria-pressed
based on (classFilter === cls), and ensure the key prop remains on the button to
preserve list identity.
pmoves/ui/e2e/archon-prompts.spec.ts-27-35 (1)

27-35: ⚠️ Potential issue | 🟠 Major

Core assertions are currently fail-open and can silently skip validation.

Patterning checks as if (count > 0) { expect(...) } means the test still passes when required controls are absent, reducing E2E signal.

✅ Proposed fix pattern
-    const searchInput = page.getByPlaceholder(/search/i);
-    if ((await searchInput.count()) > 0) {
-      await expect(searchInput.first()).toBeVisible();
-    }
+    const searchInput = page.getByPlaceholder(/search/i);
+    await expect(searchInput.first()).toBeVisible();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/archon-prompts.spec.ts` around lines 27 - 35, The current
checks for searchInput and categoryFilter are fail-open because they only assert
visibility when count > 0; replace the conditional pattern with explicit
assertions so tests fail if controls are missing: assert the locator counts
first (e.g., expect(await searchInput.count()).toBeGreaterThan(0) and
expect(await categoryFilter.count()).toBeGreaterThan(0)) and then assert the
element is visible (e.g., expect(searchInput.first()).toBeVisible() and
expect(categoryFilter.first()).toBeVisible()); update the code around the
searchInput and categoryFilter locators to remove the if(...) guards and use
these explicit expect(...) checks so absence of controls fails the test.
pmoves/ui/app/api/agents/taxonomy/route.ts-58-75 (1)

58-75: ⚠️ Potential issue | 🟠 Major

Layer coverage parser currently produces empty layer lists.

At Line 58, ([\s\*]+) only captures whitespace/asterisks, so Line 66–72 checks for L0..L5 never match. This collapses evolution staging to Base for all agents.

🐛 Proposed fix
-    const match = line.match(/([A-Z][A-Za-z-]+)\s+\*+\s+([\s\*]+)\s+(\d+)/);
+    const match = line.match(/^([A-Z][A-Za-z-]+).*?(\d+)\s*$/);
     if (match) {
       const name = match[1];
-      const layerStr = match[2];
-      const layerCount = parseInt(match[3], 10);
-
-      const layers: string[] = [];
-      if (layerStr.includes('*')) {
-        if (layerStr.includes('L0')) layers.push('L0');
-        if (layerStr.includes('L1')) layers.push('L1');
-        if (layerStr.includes('L2')) layers.push('L2');
-        if (layerStr.includes('L2.5')) layers.push('L2.5');
-        if (layerStr.includes('L3')) layers.push('L3');
-        if (layerStr.includes('L4')) layers.push('L4');
-        if (layerStr.includes('L5')) layers.push('L5');
-      }
+      const layers = Array.from(
+        new Set(line.match(/\bL(?:2\.5|[0-5])\b/g) ?? [])
+      ) as Agent['layers'];

       agentLayers.set(name, layers);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/agents/taxonomy/route.ts` around lines 58 - 75, The
layer-capture regex and subsequent guard are too narrow so layerStr never
contains the "L0".."L5" tokens; update the regex in route.ts (the const match =
line.match(...)) to capture the layer token sequence (e.g.
/([A-Z][A-Za-z-]+)\s+\*+\s+([L0-9\.]+(?:\s*[L0-9\.]*)?)\s+(\d+)/ or a safer
pattern that matches tokens like L0, L1, L2.5, etc.), then remove the
conditional that requires layerStr.includes('*') and instead check layerStr
truthiness before testing layerStr.includes('L0') ... includes('L5'); this
ensures agentLayers.set(name, layers) is populated correctly for
functions/variables name, match, layerStr, and agentLayers.
pmoves/ui/app/api/agents/taxonomy/route.ts-224-257 (1)

224-257: ⚠️ Potential issue | 🟠 Major

Signature YAML parser never builds populated sections with current gate.

Line 225 only processes lines starting with -, but section detection at Line 231 expects a word-start key. In practice, currentSection remains unset and signatures deserialize as empty/incomplete.

🐛 Proposed fix
-        for (const line of content.split('\n')) {
-          const indentMatch = line.match(/^(\s*)-/);
-          if (!indentMatch) continue;
-
-          const indent = indentMatch[1].length;
-
-          if (indent === 0) {
-            const keyMatch = line.match(/\s*(\w+):/);
-            if (keyMatch) {
-              currentSection = keyMatch[1];
-              signatures[currentSection] = {};
-            }
-          } else if (currentSection && indent === 2) {
-            const kvMatch = line.match(/\s*(\w+):\s*(.+)/);
+        for (const line of content.split('\n')) {
+          const sectionMatch = line.match(/^([A-Za-z0-9_-]+):\s*$/);
+          if (sectionMatch) {
+            currentSection = sectionMatch[1];
+            signatures[currentSection] = {};
+            continue;
+          }
+
+          const kvMatch = currentSection
+            ? line.match(/^\s{2}([A-Za-z0-9_]+):\s*(.+)\s*$/)
+            : null;
+          if (currentSection && kvMatch) {
             const key = kvMatch[1];
             let value: string | string[] | boolean = kvMatch[2].trim();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/agents/taxonomy/route.ts` around lines 224 - 257, The loop
that parses signature YAML only matches lines starting with '-' (indentMatch =
line.match(/^(\s*)-/)), so section headers never set currentSection; update the
line matching to detect leading indentation regardless of a dash (e.g., use
/^(\s*)/ or a similar regex) so indent is derived for every non-empty line, skip
empty lines, then keep the existing logic: if indent === 0 detect section
headers via keyMatch and initialize signatures[currentSection], else if
currentSection && indent === 2 parse kv pairs and coerce arrays/booleans; touch
the content.split('\n') loop and variables currentSection and signatures to
implement this.
pmoves/ui/app/api/graphiti/trails/route.ts-67-84 (1)

67-84: ⚠️ Potential issue | 🟠 Major

Don’t label entries as verified until CHIT verification actually runs.

Lines 68-83 derive isVerified and signatureValid from !!raw.sig, so any payload with a sig field is rendered as cryptographically valid even when the signature is malformed or the passphrase is unavailable.

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

In `@pmoves/ui/app/api/graphiti/trails/route.ts` around lines 67 - 84, The
toTrailEntry function currently sets isVerified and signatureValid based solely
on raw.sig; change this so entries are not marked verified until CHIT
verification runs: remove the derivation "const isVerified = !!raw.sig" and
instead initialize isVerified to false (or undefined) and signatureValid to
undefined in the returned TrailEntry so that presence of raw.sig alone does not
mark an entry verified; keep all other fields the same and ensure actual
verification logic later will set isVerified/signatureValid after validation.
pmoves/ui/app/dashboard/graphiti/page.tsx-13-33 (1)

13-33: ⚠️ Potential issue | 🟠 Major

Abort in-flight trail fetches.

Lines 13-33 start a new request on every filter change but never cancel the previous one. A slower older response can overwrite the latest filter state, and failures currently leave the previous data visible.

Suggested fix
   useEffect(() => {
+    const controller = new AbortController();
+
     const fetchTrails = async () => {
       try {
+        setError(null);
         const params = new URLSearchParams();
         if (agentFilter !== "ALL") params.set("agentId", agentFilter);
         if (verifiedOnly) params.set("verifiedOnly", "true");
 
         const res = await fetch(`/api/graphiti/trails?${params}`, {
           cache: "no-store",
+          signal: controller.signal,
         });
         if (!res.ok) throw new Error(`HTTP ${res.status}`);
         const json = await res.json();
         setData(json);
-        setError(null);
       } catch (e: unknown) {
+        if (controller.signal.aborted) return;
+        setData(null);
         setError(e instanceof Error ? e.message : String(e));
       }
     };
 
     fetchTrails();
+    return () => controller.abort();
   }, [agentFilter, verifiedOnly]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/dashboard/graphiti/page.tsx` around lines 13 - 33, The
fetchTrails useEffect needs to cancel in-flight requests and avoid stale
results: create an AbortController per fetch and pass controller.signal into the
fetch call in fetchTrails, call controller.abort() in the effect cleanup to
cancel previous requests, and in the catch block ignore abort errors (don't call
setError for aborted requests). Also clear stale data when starting a new fetch
(e.g., call setData(null) before awaiting fetch) and on non-abort failures
ensure setData(null) and setError(...) are used so old data is not left visible;
reference useEffect, fetchTrails, fetch, setData, setError, and the
AbortController signal handling.
pmoves/ui/lib/api/flute.ts-431-432 (1)

431-432: ⚠️ Potential issue | 🟠 Major

Preserve the backend’s reported health state.

Lines 431-432 coerce every 2xx response to { healthy: true }. A degraded service returning { healthy: false } will be reported as healthy in the UI.

Suggested fix
-    const data = (await response.json()) as FluteHealth;
-    return ok({ ...data, healthy: true });
+    const data = (await response.json()) as Partial<FluteHealth>;
+    return ok({
+      healthy: typeof data.healthy === 'boolean' ? data.healthy : response.ok,
+      version: data.version,
+      voiceCount: data.voiceCount,
+      engine: data.engine,
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/lib/api/flute.ts` around lines 431 - 432, The current code forces
every 2xx response to healthy: true by returning ok({ ...data, healthy: true });
change this to preserve the backend value by returning ok(data) (where data
comes from await response.json() as FluteHealth) so the UI reflects the
service's reported healthy flag; if you need a fallback when healthy is absent,
use ok({ ...data, healthy: data.healthy ?? /*fallback*/ }) instead.
pmoves/ui/lib/api/flute.ts-43-52 (1)

43-52: ⚠️ Potential issue | 🟠 Major

Derive the WebSocket URL from a parsed URL, not a string replace.

Lines 50-52 only swap :8055 for :8056. If NEXT_PUBLIC_FLUTE_GATEWAY_URL is https://host, http://host:9000, or any other non-default override, streaming still targets the HTTP port.

Suggested fix
 function getFluteWsUrl(): string {
   const envWs =
     process.env.NEXT_PUBLIC_FLUTE_WS_URL || process.env.FLUTE_WS_URL;
   if (envWs) {
     return envWs.replace(/\/$/, '');
   }
 
-  // Derive WS URL from HTTP URL
-  const httpUrl = getFluteUrl();
-  return httpUrl.replace(/^http/, 'ws').replace(':8055', ':8056');
+  const url = new URL(getFluteUrl());
+  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
+  url.port = String(FLUTE_SERVICE_CONFIG.wsPort);
+  return url.toString().replace(/\/$/, '');
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/lib/api/flute.ts` around lines 43 - 52, getFluteWsUrl currently
does string replaces and can produce wrong ports for non-default overrides;
parse the HTTP URL from getFluteUrl() with the URL constructor, set its protocol
to "ws:" for "http:" and "wss:" for "https:", and only change the port if the
parsed port is "8055" (replace with "8056"); otherwise preserve the original
port/host/path and return the formatted origin (host + optional port) or full
URL as appropriate. This uses getFluteUrl() and updates getFluteWsUrl() to
operate on a URL object rather than naive string replace.

Comment on lines +1 to +87
# Page snapshot

```yaml
- generic [active] [ref=e1]:
- link "Skip to main content" [ref=e2] [cursor=pointer]:
- /url: "#main-content"
- main [ref=e3]:
- navigation "Dashboard navigation" [ref=e4]:
- link "Ingestion" [ref=e5] [cursor=pointer]:
- /url: /dashboard/ingest
- text: Ingestion
- link "Queue" [ref=e7] [cursor=pointer]:
- /url: /dashboard/ingestion-queue
- text: Queue
- link "Videos" [ref=e9] [cursor=pointer]:
- /url: /dashboard/videos
- link "Search" [ref=e10] [cursor=pointer]:
- /url: /dashboard/search
- link "Jellyfin" [ref=e11] [cursor=pointer]:
- /url: /dashboard/jellyfin
- link "Research" [ref=e12] [cursor=pointer]:
- /url: /dashboard/research
- link "Monitor" [ref=e13] [cursor=pointer]:
- /url: /dashboard/monitor
- link "Notebook" [ref=e14] [cursor=pointer]:
- /url: /dashboard/notebook
- link "Runtime" [ref=e15] [cursor=pointer]:
- /url: /dashboard/notebook/runtime
- link "Workbench" [ref=e16] [cursor=pointer]:
- /url: /notebook-workbench
- link "Personas" [ref=e17] [cursor=pointer]:
- /url: /dashboard/personas
- link "Chat" [ref=e18] [cursor=pointer]:
- /url: /dashboard/chat
- link "Services" [ref=e19] [cursor=pointer]:
- /url: /dashboard/services
- link "Chit" [ref=e20] [cursor=pointer]:
- /url: /dashboard/chit
- link "Tokenism" [ref=e21] [cursor=pointer]:
- /url: /dashboard/tokenism
- generic [ref=e22]: Owner
- generic [ref=e23]:
- generic [ref=e24]:
- generic [ref=e25]:
- heading "Ingestion Queue" [level=1] [ref=e26]
- generic [ref=e29]: error
- button "Approval Rules" [ref=e31] [cursor=pointer]
- paragraph [ref=e32]: Review and approve content for ingestion. YouTube videos, PDFs, and URLs await your decision.
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e37]: Pending
- generic [ref=e38]: "0"
- generic [ref=e39]:
- generic [ref=e42]: Approved
- generic [ref=e43]: "0"
- generic [ref=e44]:
- generic [ref=e47]: Processing
- generic [ref=e48]: "0"
- generic [ref=e49]:
- generic [ref=e52]: Completed
- generic [ref=e53]: "0"
- generic [ref=e54]:
- generic [ref=e55]:
- generic [ref=e56]: Status
- combobox [ref=e57]:
- option "All Statuses"
- option "Pending" [selected]
- option "Approved"
- option "Processing"
- option "Completed"
- option "Rejected"
- option "Failed"
- generic [ref=e58]:
- generic [ref=e59]: Source
- combobox [ref=e60]:
- option "All Sources" [selected]
- option "YouTube"
- option "PDF"
- option "URL"
- option "Upload"
- option "Notebook"
- option "RSS"
- generic [ref=e62]:
- generic [ref=e63]: 📭
- generic [ref=e64]: No items in queue with status "pending".
- alert [ref=e65]
``` No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if test-results directory is already in .gitignore and find similar test artifact files

# Check if test-results is in .gitignore
echo "=== Checking .gitignore for test-results exclusion ==="
fd -H -t f '^\.gitignore$' -x grep -H 'test-results' {}

echo ""
echo "=== Finding other test-results artifacts in the repository ==="
# Find other test-results directories/files
fd -t d 'test-results' --max-depth 5

echo ""
echo "=== Count of error-context.md files ==="
fd -t f 'error-context.md' | wc -l

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 254


🏁 Script executed:

# Check if test-results directory/files are actually tracked by git
echo "=== Checking if test-results is tracked by git ==="
git ls-files | grep 'test-results' | head -20

echo ""
echo "=== Total files tracked under test-results ==="
git ls-files | grep 'test-results' | wc -l

echo ""
echo "=== Check .gitignore for any test-related patterns ==="
cat .gitignore 2>/dev/null | grep -E '(test|spec|playwright)'

echo ""
echo "=== Check if test-results was recently added ==="
git log --oneline -20 -- '**/test-results'

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 2452


Exclude test artifacts from version control.

Test result files are currently being tracked in git—351 files across pmoves/ui/test-results/ including error snapshots (.md), screenshots (.png), and execution traces (.zip). These auto-generated artifacts should be excluded via .gitignore because they:

  • Are regenerated on every test run
  • Bloat repository size and clone time indefinitely
  • Are not needed for code review or reproducibility

Add pmoves/ui/test-results/ to .gitignore. Also consider removing these files from git history using git rm -r --cached pmoves/ui/test-results/ to prevent future clones from downloading test artifacts.

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

In
`@pmoves/ui/test-results/ingestion-Enhanced-Video-A-a824d-ld-select-all-visible-items-chromium/error-context.md`
around lines 1 - 87, The repo is tracking generated test artifacts under
pmoves/ui/test-results/ (error-context.md, .png, .zip) which should be ignored;
add the path pmoves/ui/test-results/ to .gitignore, commit that change, then
remove the tracked files from the index with git rm -r --cached
pmoves/ui/test-results/ and commit the removal so future clones don't download
these artifacts (optionally run git gc or rewrite history if you must purge them
from past commits).

Comment on lines +1 to +80
# Page snapshot

```yaml
- generic [active] [ref=e1]:
- link "Skip to main content" [ref=e2] [cursor=pointer]:
- /url: "#main-content"
- main [ref=e3]:
- navigation "Dashboard navigation" [ref=e4]:
- link "Ingestion" [ref=e5] [cursor=pointer]:
- /url: /dashboard/ingest
- link "Queue" [ref=e6] [cursor=pointer]:
- /url: /dashboard/ingestion-queue
- link "Videos" [ref=e7] [cursor=pointer]:
- /url: /dashboard/videos
- link "Search" [ref=e8] [cursor=pointer]:
- /url: /dashboard/search
- link "Jellyfin" [ref=e9] [cursor=pointer]:
- /url: /dashboard/jellyfin
- link "Research" [ref=e10] [cursor=pointer]:
- /url: /dashboard/research
- text: Research
- link "Monitor" [ref=e12] [cursor=pointer]:
- /url: /dashboard/monitor
- link "Notebook" [ref=e13] [cursor=pointer]:
- /url: /dashboard/notebook
- link "Runtime" [ref=e14] [cursor=pointer]:
- /url: /dashboard/notebook/runtime
- link "Workbench" [ref=e15] [cursor=pointer]:
- /url: /notebook-workbench
- link "Personas" [ref=e16] [cursor=pointer]:
- /url: /dashboard/personas
- link "Chat" [ref=e17] [cursor=pointer]:
- /url: /dashboard/chat
- link "Services" [ref=e18] [cursor=pointer]:
- /url: /dashboard/services
- link "Chit" [ref=e19] [cursor=pointer]:
- /url: /dashboard/chit
- link "Tokenism" [ref=e20] [cursor=pointer]:
- /url: /dashboard/tokenism
- generic [ref=e21]: Owner
- generic [ref=e22]:
- heading "Deep Research" [level=1] [ref=e23]
- paragraph [ref=e24]: Initiate and manage deep research tasks using PMOVES AI research orchestration.
- generic [ref=e26]: "DeepResearch: Disconnected"
- generic [ref=e27]:
- generic [ref=e28]:
- heading "Start New Research" [level=3] [ref=e29]
- button "Expand options" [ref=e30] [cursor=pointer]:
- img [ref=e31]
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]: Research Question
- textbox "Research Question" [ref=e36]:
- /placeholder: Enter your research question...
- generic [ref=e37]: 0 / 1000
- button "Start Research" [disabled] [ref=e39]
- generic [ref=e40]:
- generic [ref=e42]:
- generic [ref=e43]:
- generic [ref=e44]:
- heading "Research Tasks" [level=2] [ref=e45]
- generic [ref=e46]: 0 of 0
- generic [ref=e47]:
- combobox [ref=e48]:
- option "All" [selected]
- option "Pending"
- option "Running"
- option "Completed"
- option "Failed"
- option "Cancelled"
- button "Refresh tasks" [ref=e49] [cursor=pointer]: Refresh
- generic [ref=e51]:
- generic [ref=e52]: 🔬
- paragraph [ref=e53]: No research tasks yet.
- paragraph [ref=e54]: Start one above!
- generic [ref=e56]:
- generic [ref=e57]: 🔬
- paragraph [ref=e58]: Select a task to view details
- alert [ref=e59]
``` No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Test artifacts should not be committed to version control.

This file appears to be a test error snapshot or result artifact (indicated by the test-results/ directory and error-context.md filename). Test results are ephemeral build outputs that should remain local-only. Committing them creates repository pollution, merge conflicts, and maintenance overhead.

Additionally, the coding guidelines specify that .md files should document NATS event topology or services catalog, not serve as test snapshots.

🧹 Recommended fix
  1. Remove this file and the entire pmoves/ui/test-results/ directory from the PR:
git rm -r pmoves/ui/test-results/
  1. Add test results to .gitignore (in pmoves/ui/.gitignore or root .gitignore):
+# Playwright test results
+test-results/
+playwright-report/
  1. If you need to preserve test snapshots for debugging, keep them in your local environment only or use CI artifact storage.

As per coding guidelines: "**/*.md: Document NATS event topology in .claude/context/nats-subjects.md and maintain services catalog with port assignments and health endpoints in .claude/context/services-catalog.md"

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

In
`@pmoves/ui/test-results/research-Deep-Research-Dashboard-should-clear-task-selection-chromium/error-context.md`
around lines 1 - 80, This commit includes test artifact file
pmoves/ui/test-results/research-Deep-Research-Dashboard-should-clear-task-selection-chromium/error-context.md
which should not be checked in; remove the file and the entire
pmoves/ui/test-results/ directory from the PR (e.g., git rm -r
pmoves/ui/test-results/), update the appropriate .gitignore
(pmoves/ui/.gitignore or root .gitignore) to exclude test-results/ and any
similar snapshot patterns, and ensure you do not replace intended Markdown docs:
keep NATS and services documentation only in .claude/context/nats-subjects.md
and .claude/context/services-catalog.md as per repository conventions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (3)
pmoves/ui/app/api/services/health-enhanced/route.ts (2)

107-125: Filtering mismatch: servicesToCheck is filtered but checkAllServices ignores tier/agentsOnly filters.

The code filters servicesToCheck by tierParam (line 107-111) and agentsOnly (line 117-119), but checkAllServices (line 122-125) only receives categoryParam. This means:

  • If filtering by tier or agentsOnly, the health check still probes all services (or category-filtered set), potentially wasting time on services that won't be included in the response.

Consider passing the filtered slugs to checkAllServices to avoid unnecessary health probes:

♻️ Pass filtered slugs to checkAllServices
     // Perform health checks
+    const slugsToCheck = servicesToCheck.map((s) => s.slug);
     const healthResult: HealthCheckResult = await checkAllServices(
       timeout,
-      categoryParam ? { category: categoryParam as any } : undefined
+      slugsToCheck.length < SERVICE_CATALOG.length
+        ? { slugs: slugsToCheck }
+        : categoryParam
+          ? { category: categoryParam }
+          : undefined
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/services/health-enhanced/route.ts` around lines 107 - 125,
The filtering applied to servicesToCheck (using tierParam/TIER_CATEGORIES,
categoryParam, and agentsOnly/AGENT_SLUGS) isn't propagated into the health
probes—update the checkAllServices call to accept and use the filtered slugs so
only those services are checked: compute const filteredSlugs =
servicesToCheck.map(s => s.slug) after the existing filters and pass it into
checkAllServices (e.g., checkAllServices(timeout, { category: categoryParam as
any, slugs: filteredSlugs })), and if needed update the checkAllServices
signature to accept an options object with a slugs: string[] field so
HealthCheckResult remains accurate.

124-124: Avoid as any cast; use proper typing.

categoryParam is already string | null, and when truthy it's a string. The as any cast silences type checking unnecessarily. If there's a type mismatch with checkAllServices, fix the types explicitly rather than casting to any.

🧹 Remove unnecessary cast
     const healthResult: HealthCheckResult = await checkAllServices(
       timeout,
-      categoryParam ? { category: categoryParam as any } : undefined
+      categoryParam ? { category: categoryParam } : undefined
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/services/health-enhanced/route.ts` at line 124, Remove the
unnecessary "as any" cast on categoryParam and propagate correct types: in the
call that builds the object (currently "categoryParam ? { category:
categoryParam as any } : undefined"), pass the string directly (e.g. { category:
categoryParam ?? undefined } or categoryParam ? { category: categoryParam } :
undefined) and update the target function signature (checkAllServices) to accept
category?: string | undefined (or string | null if you prefer) so no casting is
needed; adjust any related types/interfaces to expect a string/undefined rather
than any.
pmoves/ui/app/api/graphiti/trails/route.ts (1)

67-85: Signature verification is currently a placeholder.

The isVerified flag (line 68) only checks for the presence of raw.sig, not its cryptographic validity. signatureValid is unconditionally set to true when a signature exists (line 83). This means:

  • Any entry with a sig field will be marked as verified, even if the signature is invalid or malformed.

Consider documenting this limitation or marking it as a TODO if actual verification is planned.

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

In `@pmoves/ui/app/api/graphiti/trails/route.ts` around lines 67 - 85, The code in
toTrailEntry currently treats any presence of raw.sig as a valid signature
(isVerified and signatureValid), which is incorrect; update to perform real
cryptographic verification (or if not available yet, don't assume validity):
replace the simplistic isVerified = !!raw.sig and signatureValid assignment with
a verification step that validates raw.sig against the payload using your
project's verifier function (or call a new verifySignature(raw) helper) and set
signatureValid to the boolean result; if verification isn't implemented now,
change the logic to set isVerified = !!raw.sig but set signatureValid =
undefined and add a clear TODO comment in toTrailEntry referencing
verifySignature/raw.sig so future implementers know to hook up actual
verification.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pmoves/ui/app/api/agents/taxonomy/route.ts`:
- Around line 347-351: The code uses a non-null assertion on a.secondaryType
when filtering agents; replace this with an explicit undefined check so
TypeScript is correct and intent is clear: in the filter that references
typeFilter, test a.secondaryType !== undefined (or a.secondaryType != null)
before calling typeFilter.includes on it, or combine checks so you call
typeFilter.includes(a.primaryType) || (a.secondaryType !== undefined &&
typeFilter.includes(a.secondaryType)); update the filter logic that references
agents, typeFilter, and a.secondaryType accordingly.
- Around line 223-254: The current line-by-line parser in the loop that iterates
over content.split('\n') (inside the code that builds the signatures object)
incorrectly matches lines with /^(\s*)-/ and fails to parse normal YAML; replace
this custom parser with a real YAML parser (e.g., import parse from 'yaml' or
safeLoad from 'js-yaml'), call the parser on the whole content to get a JS
object, then pull signatures (e.g., parsed.signatures or parsed?.signatures)
into the existing signatures map; update imports, remove the manual line parsing
loop, and add simple error handling around the YAML parse so
loadSignatures()/the route returns a meaningful error when parsing fails.

In `@pmoves/ui/app/api/github/prs/route.ts`:
- Around line 136-145: The 503 error response in prs/route.ts returns a partial
object and must match the full PRListResponse shape; update the
NextResponse.json call in the token-unavailable branch to include the same
fields as the success/500 paths (open, merged, closed, draft, items, total,
timestamp, error, message) so callers typed to PRListResponse always see a
consistent schema; locate the token check that returns "GitHub token
unavailable" and add default values for open/merged/closed/draft (e.g.,
zeros/false) to mirror the 500 branch.
- Line 125: Validate and clamp the parsed limit value before slicing: when
reading searchParams.get('limit') and using parseInt(..., 10) to set the const
limit, ensure the parsed value is a finite positive integer (e.g.,
Number.isFinite/Number.isInteger and > 0); if it is NaN or <= 0, fall back to
50, then cap the result at 100 (use Math.min on the validated value and 100, and
Math.max to prevent negatives if needed). Update the logic around the existing
parseInt/searchParams.get usage so that limit is always a positive integer
between 1 and 100 with default 50.
- Around line 9-10: The edge route hard-codes a loopback URL for fetching the
GitHub token (e.g., fetching http://localhost:8091) while also declaring export
const runtime = 'edge'; update the handler to instead read an ARCHON endpoint
from an environment variable (for example process.env.NEXT_PUBLIC_ARCHON_URL or
process.env.ARCHON_BASE_URL) and only fall back to localhost for local dev, or
if the service is colocated switch runtime to 'nodejs' (change export const
runtime = 'edge' to 'nodejs'); locate the fetch call that contacts the loopback
(and the top-level export const runtime/dynamic declarations) and replace the
hard-coded URL with the env-based URL/fallback or change the runtime accordingly
so Vercel Edge does not ignore the request port.
- Around line 44-47: The current request sends unsupported GitHub REST states
(e.g., 'MERGED') and never derives or filters by merged/draft info; change the
fetch to use state=open or state=all (set in stateFilter/url), update mapState
to compute PRStatus.state using pr.merged_at and pr.draft (not just pr.state),
then post-filter the returned list by the original requested states array so
only matching PRs are returned, and finally compute the merged/draft/open
counters from those derived PRStatus values (the logic referenced by mapState
and the counters must be updated accordingly).

In `@pmoves/ui/app/api/graphiti/trails/route.ts`:
- Line 146: The current computation of limit using const limit =
Math.min(parseInt(searchParams.get('limit') || '50', 10), 200) can produce NaN
for non-numeric inputs; update the logic around the limit variable to parse the
value safely (e.g., parseInt or Number), check for NaN (Number.isNaN or isNaN),
and fall back to the default 50 before clamping to 200 so limit is always a
valid integer (used later in slice(0, limit)). Locate the expression creating
limit in route.ts, replace it with a safe-parse + default + clamp sequence, and
ensure limit is an integer >= 0.

In `@pmoves/ui/components/DashboardNavigation.tsx`:
- Around line 50-53: NAV_ITEMS is missing entries for the NavKey values
'agents', 'github', and 'graphiti', so add objects for each to the NAV_ITEMS
array (used by the DashboardNavigation component) so links render and active
highlighting works; specifically add entries with href '/dashboard/agents',
'/dashboard/github', '/dashboard/graphiti' and labels 'Agents', 'GitHub',
'Graphiti' and keys 'agents', 'github', 'graphiti' respectively so the item.key
=== active check can match.

---

Nitpick comments:
In `@pmoves/ui/app/api/graphiti/trails/route.ts`:
- Around line 67-85: The code in toTrailEntry currently treats any presence of
raw.sig as a valid signature (isVerified and signatureValid), which is
incorrect; update to perform real cryptographic verification (or if not
available yet, don't assume validity): replace the simplistic isVerified =
!!raw.sig and signatureValid assignment with a verification step that validates
raw.sig against the payload using your project's verifier function (or call a
new verifySignature(raw) helper) and set signatureValid to the boolean result;
if verification isn't implemented now, change the logic to set isVerified =
!!raw.sig but set signatureValid = undefined and add a clear TODO comment in
toTrailEntry referencing verifySignature/raw.sig so future implementers know to
hook up actual verification.

In `@pmoves/ui/app/api/services/health-enhanced/route.ts`:
- Around line 107-125: The filtering applied to servicesToCheck (using
tierParam/TIER_CATEGORIES, categoryParam, and agentsOnly/AGENT_SLUGS) isn't
propagated into the health probes—update the checkAllServices call to accept and
use the filtered slugs so only those services are checked: compute const
filteredSlugs = servicesToCheck.map(s => s.slug) after the existing filters and
pass it into checkAllServices (e.g., checkAllServices(timeout, { category:
categoryParam as any, slugs: filteredSlugs })), and if needed update the
checkAllServices signature to accept an options object with a slugs: string[]
field so HealthCheckResult remains accurate.
- Line 124: Remove the unnecessary "as any" cast on categoryParam and propagate
correct types: in the call that builds the object (currently "categoryParam ? {
category: categoryParam as any } : undefined"), pass the string directly (e.g. {
category: categoryParam ?? undefined } or categoryParam ? { category:
categoryParam } : undefined) and update the target function signature
(checkAllServices) to accept category?: string | undefined (or string | null if
you prefer) so no casting is needed; adjust any related types/interfaces to
expect a string/undefined rather than any.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7426e602-5b1f-4746-9f55-8a5fd7d810d7

📥 Commits

Reviewing files that changed from the base of the PR and between 8c1f1d1 and 5cb1ecc.

📒 Files selected for processing (6)
  • pmoves/ui/app/api/agents/taxonomy/route.ts
  • pmoves/ui/app/api/github/prs/route.ts
  • pmoves/ui/app/api/graphiti/trails/route.ts
  • pmoves/ui/app/api/services/health-enhanced/route.ts
  • pmoves/ui/app/dashboard/ingestion-queue/page.tsx
  • pmoves/ui/components/DashboardNavigation.tsx
💤 Files with no reviewable changes (1)
  • pmoves/ui/app/dashboard/ingestion-queue/page.tsx

Comment thread pmoves/ui/app/api/agents/taxonomy/route.ts
Comment on lines +223 to +254
for (const line of content.split('\n')) {
const indentMatch = line.match(/^(\s*)-/);
if (!indentMatch) continue;

const indent = indentMatch[1].length;

if (indent === 0) {
const keyMatch = line.match(/\s*(\w+):/);
if (keyMatch) {
currentSection = keyMatch[1];
signatures[currentSection] = {};
}
} else if (currentSection && indent === 2) {
const kvMatch = line.match(/\s*(\w+):\s*(.+)/);
if (kvMatch) {
const key = kvMatch[1];
let value: string | string[] | boolean = kvMatch[2].trim();

// Handle array values
if (typeof value === 'string' && value.startsWith('[')) {
value = value
.slice(1, -1)
.split(',')
.map((v) => v.trim().replace(/['"]/g, ''));
} else if (value === 'true' || value === 'false') {
value = value === 'true';
}

signatures[currentSection][key] = value;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find agent_signatures.yaml and check its actual format
fd -t f 'agent_signatures.yaml' --exec head -30 {}

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1180


🏁 Script executed:

#!/bin/bash
# Check the loadSignatures function context and how it handles the result
sed -n '205,267p' pmoves/ui/app/api/agents/taxonomy/route.ts

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 2029


🏁 Script executed:

#!/bin/bash
# Check how signatures are actually used after being parsed
rg -A 10 'loadSignatures' pmoves/ui/app/api/agents/taxonomy/route.ts | head -50

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 669


🏁 Script executed:

#!/bin/bash
# Check if loadSignatures result is actually used and how errors are handled
rg -B 5 -A 15 'const.*signatures.*=' pmoves/ui/app/api/agents/taxonomy/route.ts | grep -A 20 'loadSignatures'

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 369


🏁 Script executed:

#!/bin/bash
# Check what the default/fallback signatures are when empty
rg -A 20 'signatures.*\[\]|signatures.*{|if.*!signatures|if.*!.*signatures' pmoves/ui/app/api/agents/taxonomy/route.ts

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 243


🏁 Script executed:

#!/bin/bash
# Get more context around the loadSignatures call
sed -n '100,160p' pmoves/ui/app/api/agents/taxonomy/route.ts

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 2006


🏁 Script executed:

#!/bin/bash
# Search for how signatures variable is used after loading
rg 'signatures' pmoves/ui/app/api/agents/taxonomy/route.ts | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 769


🏁 Script executed:

#!/bin/bash
# Verify no other mechanisms load signatures
rg 'agent_signatures|signatures' pmoves/ui/app/api/agents/taxonomy/route.ts | grep -v '//'

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 769


🏁 Script executed:

#!/bin/bash
# Check if signatures are used elsewhere in the codebase
rg 'from.*loadSignatures|import.*loadSignatures' pmoves/ui/app/api/agents/

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 49


Custom YAML parser has flawed logic and completely breaks signature loading.

The parser checks for lines starting with - (line 224), but standard YAML key-value pairs don't start with - (that's only for array items). For a YAML structure like:

signatures:
  claude-opus:
    agent_id: "claude-opus"
    display_name: "Agent Zero"
    resonance:
      - security-audit

The indentMatch regex (/^(\s*)-/) will only match array items like - security-audit, never the agent keys themselves. This causes loadSignatures() to return an empty object, making all agent signatures undefined in the response.

Replace with a proper YAML parser like js-yaml or yaml.

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

In `@pmoves/ui/app/api/agents/taxonomy/route.ts` around lines 223 - 254, The
current line-by-line parser in the loop that iterates over content.split('\n')
(inside the code that builds the signatures object) incorrectly matches lines
with /^(\s*)-/ and fails to parse normal YAML; replace this custom parser with a
real YAML parser (e.g., import parse from 'yaml' or safeLoad from 'js-yaml'),
call the parser on the whole content to get a JS object, then pull signatures
(e.g., parsed.signatures or parsed?.signatures) into the existing signatures
map; update imports, remove the manual line parsing loop, and add simple error
handling around the YAML parse so loadSignatures()/the route returns a
meaningful error when parsing fails.

Comment thread pmoves/ui/app/api/agents/taxonomy/route.ts
Comment on lines +9 to +10
export const runtime = 'edge';
export const dynamic = 'force-dynamic';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "route.ts" | grep -i github | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 106


🏁 Script executed:

find . -type f -path "*pmoves/ui/app/api/github/prs/route.ts"

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 106


🏁 Script executed:

fd "route.ts" -p "pmoves"

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1110


🏁 Script executed:

fd "route.ts" | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 896


🏁 Script executed:

git ls-files | grep -E "github.*route\.ts|prs.*route\.ts" | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 104


🏁 Script executed:

git ls-files | grep -i "github" | head -30

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1183


🏁 Script executed:

cat -n ./pmoves/ui/app/api/github/prs/route.ts

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 7409


🏁 Script executed:

rg "ARCHON_BASE_URL|localhost.*8091" --type ts --type js

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 460


🏁 Script executed:

rg "archon.*url|ARCHON.*URL" -i --type ts --type js | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 476


🏁 Script executed:

rg "process\.env\." pmoves/ui/app/api/ -A 2 | grep -E "ARCHON|archon" | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 49


🏁 Script executed:

grep -r "process.env" pmoves/ui/app/api/ --include="*.ts" | grep -i "base\|url" | head -15

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1982


Replace hard-coded loopback address with environment variable in edge route.

This handler uses runtime = 'edge', but attempts to fetch a GitHub token from http://localhost:8091, which is unreliable on Vercel Edge (non-standard fetch ports are ignored). This pattern is not followed elsewhere in the codebase—other API routes check environment variables first with a localhost fallback.

Use runtime = 'nodejs' if colocation with Archon is guaranteed, or follow the established pattern: read an ARCHON endpoint from environment (e.g., process.env.NEXT_PUBLIC_ARCHON_URL or process.env.ARCHON_BASE_URL) and fall back to localhost only for local development.

Suggested direction
-export const runtime = 'edge';
+export const runtime = 'nodejs';
@@
 async function getGitHubToken(): Promise<string | null> {
   // Try to get token from Archon service which has minting capability
   try {
-    const archonResponse = await fetch('http://localhost:8091/api/github/token', {
+    const archonBaseUrl = process.env.ARCHON_BASE_URL || process.env.NEXT_PUBLIC_ARCHON_URL || 'http://localhost:8091';
+
+    const archonResponse = await fetch(new URL('/api/github/token', archonBaseUrl), {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       signal: AbortSignal.timeout(5000),
     });

Also applies to: lines 96-100

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

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 9 - 10, The edge route
hard-codes a loopback URL for fetching the GitHub token (e.g., fetching
http://localhost:8091) while also declaring export const runtime = 'edge';
update the handler to instead read an ARCHON endpoint from an environment
variable (for example process.env.NEXT_PUBLIC_ARCHON_URL or
process.env.ARCHON_BASE_URL) and only fall back to localhost for local dev, or
if the service is colocated switch runtime to 'nodejs' (change export const
runtime = 'edge' to 'nodejs'); locate the fetch call that contacts the loopback
(and the top-level export const runtime/dynamic declarations) and replace the
hard-coded URL with the env-based URL/fallback or change the runtime accordingly
so Vercel Edge does not ignore the request port.

Comment on lines +44 to +47
const stateFilter = states.includes('MERGED') ? 'MERGED' : states.includes('CLOSED') ? 'CLOSED' : 'OPEN';

// Use GitHub REST API for simplicity (no GraphQL complexity)
const url = `https://api.github.com/repos/${GITHUB_ORG}/${repo}/pulls?state=${stateFilter}&per_page=100&sort=updated&direction=desc`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file
find . -name "route.ts" | grep -E "github/prs"

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 106


🏁 Script executed:

# Get the full file content with line numbers to see context
wc -l pmoves/ui/app/api/github/prs/route.ts

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 108


🏁 Script executed:

# Read the file section containing the code in question
sed -n '40,170p' pmoves/ui/app/api/github/prs/route.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 5264


🏁 Script executed:

# Check the mapState function signature and implementation
sed -n '60,85p' pmoves/ui/app/api/github/prs/route.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1016


🏁 Script executed:

# Check the types to understand the PRStatus structure
find . -name "github.ts" | grep types

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 98


🌐 Web query:

GitHub REST API pull requests list state parameter documentation

💡 Result:

For the “List pull requests” endpoint:

Documentation: (docs.github.com)

Citations:


🏁 Script executed:

# Find the mapState function definition
sed -n '1,60p' pmoves/ui/app/api/github/prs/route.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 2340


🏁 Script executed:

# Check the entire file structure to understand the flow
head -50 pmoves/ui/app/api/github/prs/route.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1956


🏁 Script executed:

# Read the types file to understand PRState and PRStatus
cat pmoves/ui/lib/types/github.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 3480


🏁 Script executed:

# Check the complete fetchRepoPRs function
sed -n '39,75p' pmoves/ui/app/api/github/prs/route.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1483


🏁 Script executed:

# Check if there's any post-filtering in the handler and verify counter logic
sed -n '110,135p' pmoves/ui/app/api/github/prs/route.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1075


🏁 Script executed:

# Verify complete handler flow - check if any filtering happens after fetchRepoPRs
sed -n '100,135p' pmoves/ui/app/api/github/prs/route.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1352


🏁 Script executed:

# Get the complete handler including response construction
sed -n '120,160p' pmoves/ui/app/api/github/prs/route.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1776


🏁 Script executed:

# Get lines around 160-165 to see the full response structure and counters
sed -n '140,170p' pmoves/ui/app/api/github/prs/route.ts | cat -n

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1414


Fix state filtering and derivation for GitHub REST API contract.

GET /repos/{owner}/{repo}/pulls only supports state=open|closed|all, not MERGED or DRAFT. The code attempts to pass state=MERGED (line 44), which the API rejects. Additionally, pr.merged_at and pr.draft fields are captured but never used to derive state—mapState only looks at pr.state and its cases for MERGED/DRAFT are unreachable dead code. No post-filtering by requested states occurs, so the returned items don't match user filters, and counters (lines 163–166) will always show zero merged PRs. Fetch with state=open or state=all, derive PRStatus.state from pr.merged_at and pr.draft, then filter results by the requested states array in-process. (docs.github.com)

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

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 44 - 47, The current
request sends unsupported GitHub REST states (e.g., 'MERGED') and never derives
or filters by merged/draft info; change the fetch to use state=open or state=all
(set in stateFilter/url), update mapState to compute PRStatus.state using
pr.merged_at and pr.draft (not just pr.state), then post-filter the returned
list by the original requested states array so only matching PRs are returned,
and finally compute the merged/draft/open counters from those derived PRStatus
values (the logic referenced by mapState and the counters must be updated
accordingly).

Comment thread pmoves/ui/app/api/github/prs/route.ts Outdated
const searchParams = request.nextUrl.searchParams;
const stateParam = searchParams.get('state')?.toUpperCase().split(',') || ['OPEN', 'DRAFT'];
const repoFilter = searchParams.get('repo');
const limit = Math.min(parseInt(searchParams.get('limit') || '50', 10), 100);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate limit before applying it.

parseInt() can produce NaN, and negative values survive Math.min(). Today ?limit=foo becomes an empty list and ?limit=-5 slices off the last five items. Clamp to a positive integer or fall back to 50.

Suggested fix
-  const limit = Math.min(parseInt(searchParams.get('limit') || '50', 10), 100);
+  const parsedLimit = Number.parseInt(searchParams.get('limit') ?? '', 10);
+  const limit = Number.isFinite(parsedLimit)
+    ? Math.min(Math.max(parsedLimit, 1), 100)
+    : 50;
📝 Committable suggestion

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

Suggested change
const limit = Math.min(parseInt(searchParams.get('limit') || '50', 10), 100);
const parsedLimit = Number.parseInt(searchParams.get('limit') ?? '', 10);
const limit = Number.isFinite(parsedLimit)
? Math.min(Math.max(parsedLimit, 1), 100)
: 50;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/github/prs/route.ts` at line 125, Validate and clamp the
parsed limit value before slicing: when reading searchParams.get('limit') and
using parseInt(..., 10) to set the const limit, ensure the parsed value is a
finite positive integer (e.g., Number.isFinite/Number.isInteger and > 0); if it
is NaN or <= 0, fall back to 50, then cap the result at 100 (use Math.min on the
validated value and 100, and Math.max to prevent negatives if needed). Update
the logic around the existing parseInt/searchParams.get usage so that limit is
always a positive integer between 1 and 100 with default 50.

Comment on lines +136 to +145
return NextResponse.json(
{
error: 'GitHub token unavailable',
message: 'Archon service unavailable or GitHub App not configured',
items: [],
timestamp: new Date().toISOString(),
total: 0,
},
{ status: 503 }
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Return the full PRListResponse shape on 503.

The 503 branch omits open, merged, closed, and draft, even though the success path and the 500 branch include them. Any caller typed against pmoves/ui/lib/types/github.ts gets a different schema exactly when the token service fails.

Suggested fix
         {
           error: 'GitHub token unavailable',
           message: 'Archon service unavailable or GitHub App not configured',
           items: [],
           timestamp: new Date().toISOString(),
           total: 0,
+          open: 0,
+          merged: 0,
+          closed: 0,
+          draft: 0,
         },
📝 Committable suggestion

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

Suggested change
return NextResponse.json(
{
error: 'GitHub token unavailable',
message: 'Archon service unavailable or GitHub App not configured',
items: [],
timestamp: new Date().toISOString(),
total: 0,
},
{ status: 503 }
);
return NextResponse.json(
{
error: 'GitHub token unavailable',
message: 'Archon service unavailable or GitHub App not configured',
items: [],
timestamp: new Date().toISOString(),
total: 0,
open: 0,
merged: 0,
closed: 0,
draft: 0,
},
{ status: 503 }
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 136 - 145, The 503 error
response in prs/route.ts returns a partial object and must match the full
PRListResponse shape; update the NextResponse.json call in the token-unavailable
branch to include the same fields as the success/500 paths (open, merged,
closed, draft, items, total, timestamp, error, message) so callers typed to
PRListResponse always see a consistent schema; locate the token check that
returns "GitHub token unavailable" and add default values for
open/merged/closed/draft (e.g., zeros/false) to mirror the 500 branch.

Comment thread pmoves/ui/app/api/graphiti/trails/route.ts Outdated
Comment on lines +50 to +53
| 'tokenism'
| 'agents'
| 'github'
| 'graphiti';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if dashboard pages exist for the new NavKey values
fd -t f 'page.tsx' --full-path '.*dashboard.*(agents|github|graphiti).*'

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 193


🏁 Script executed:

# First, find the DashboardNavigation.tsx file
find . -name "DashboardNavigation.tsx" -type f

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 113


🏁 Script executed:

# Then read the file to see current NavKey and NAV_ITEMS
# Once we find it, we'll examine the content
cat -n "pmoves/ui/components/DashboardNavigation.tsx"

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 7559


🏁 Script executed:

# Search for dashboard pages (page.tsx or similar) related to agents, github, graphiti
fd -type f "page\.tsx" --path "*dashboard*"

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 237


🏁 Script executed:

# Also search for any references to 'agents', 'github', 'graphiti' as NavKey values
rg "agents|github|graphiti" -A 3 -B 3 "pmoves/ui/components/"

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 11748


🏁 Script executed:

# Search for dashboard pages by looking for page.tsx files
find . -path "*/dashboard/*" -name "page.tsx" -type f | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 951


🏁 Script executed:

# Search for any route definitions or usage of 'agents', 'github', 'graphiti' as NavKey
rg "agents|github|graphiti" --type tsx --type ts -B 2 -A 2 | grep -E "(dashboard|NavKey|href|page)" | head -50

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 94


🏁 Script executed:

# Check if there are routes defined for /dashboard/agents, /dashboard/github, /dashboard/graphiti
find . -path "*/dashboard/agents*" -o -path "*/dashboard/github*" -o -path "*/dashboard/graphiti*" 2>/dev/null

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 295


🏁 Script executed:

# Check agents page to see if it uses DashboardHeader with active prop
cat -n "./pmoves/ui/app/dashboard/agents/page.tsx"

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 11632


🏁 Script executed:

# Check github page
cat -n "./pmoves/ui/app/dashboard/github/page.tsx"

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 9087


🏁 Script executed:

# Check graphiti page
cat -n "./pmoves/ui/app/dashboard/graphiti/page.tsx"

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 7769


Add missing NAV_ITEMS entries for agents, github, and graphiti dashboard pages.

The NavKey union now includes 'agents', 'github', and 'graphiti', but NAV_ITEMS (lines 17-33) has no corresponding entries. These pages already exist and correctly pass the active prop, but:

  1. No navigation links will render for these pages.
  2. Active state highlighting won't work since line 75 checks item.key === active, which will never match.

Add entries to NAV_ITEMS for the three dashboard pages:

Example entries
  { href: '/dashboard/agents', label: 'Agents', key: 'agents' },
  { href: '/dashboard/github', label: 'GitHub', key: 'github' },
  { href: '/dashboard/graphiti', label: 'Graphiti', key: 'graphiti' },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/components/DashboardNavigation.tsx` around lines 50 - 53, NAV_ITEMS
is missing entries for the NavKey values 'agents', 'github', and 'graphiti', so
add objects for each to the NAV_ITEMS array (used by the DashboardNavigation
component) so links render and active highlighting works; specifically add
entries with href '/dashboard/agents', '/dashboard/github',
'/dashboard/graphiti' and labels 'Agents', 'GitHub', 'Graphiti' and keys
'agents', 'github', 'graphiti' respectively so the item.key === active check can
match.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (7)
pmoves/ui/app/api/agents/taxonomy/route.ts (2)

215-267: ⚠️ Potential issue | 🔴 Critical

loadSignatures parser logic skips normal YAML mappings, so signatures are effectively never loaded.

Line 234 requires - list-item lines, but signature definitions are mapping keys. The parser then misses agent records and typically returns {}.

💡 Proposed fix (use real YAML parse instead of line-by-line heuristic)
 async function loadSignatures(): Promise<Record<string, any>> {
   try {
     const fs = await import('fs/promises');
     const path = await import('path');
+    const YAML = await import('yaml');

     // Try both absolute and relative paths
     const possiblePaths = [
       path.join(process.cwd(), 'pmoves', 'config', 'agent_signatures.yaml'),
       path.join(process.cwd(), '..', '..', 'pmoves', 'config', 'agent_signatures.yaml'),
     ];

     for (const yamlPath of possiblePaths) {
       try {
         const content = await fs.readFile(yamlPath, 'utf-8');
-        // Simple YAML parser for our structure
-        const signatures: Record<string, any> = {};
-        let currentSection: string | null = null;
-
-        for (const line of content.split('\n')) {
-          const indentMatch = line.match(/^(\s*)-/);
-          if (!indentMatch) continue;
-
-          const indent = indentMatch[1].length;
-
-          if (indent === 0) {
-            const keyMatch = line.match(/\s*(\w+):/);
-            if (keyMatch) {
-              currentSection = keyMatch[1];
-              signatures[currentSection] = {};
-            }
-          } else if (currentSection && indent === 2) {
-            const kvMatch = line.match(/\s*(\w+):\s*(.+)/);
-            if (kvMatch) {
-              const key = kvMatch[1];
-              let value: string | string[] | boolean = kvMatch[2].trim();
-
-              // Handle array values
-              if (typeof value === 'string' && value.startsWith('[')) {
-                value = value
-                  .slice(1, -1)
-                  .split(',')
-                  .map((v) => v.trim().replace(/['"]/g, ''));
-              } else if (value === 'true' || value === 'false') {
-                value = value === 'true';
-              }
-
-              signatures[currentSection][key] = value;
-            }
-          }
-        }
-
-        return signatures;
+        const parsed = YAML.parse(content) as Record<string, any> | null;
+        if (!parsed || typeof parsed !== 'object') continue;
+
+        const candidate = parsed.signatures && typeof parsed.signatures === 'object'
+          ? parsed.signatures
+          : parsed;
+
+        if (candidate && typeof candidate === 'object') {
+          return candidate as Record<string, any>;
+        }
       } catch {
         // Try next path
         continue;
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/agents/taxonomy/route.ts` around lines 215 - 267, The
custom line-by-line parser in loadSignatures incorrectly expects '-' list items
and misses normal YAML mappings, resulting in empty signatures; replace the
heuristic parsing with a proper YAML parser: import fs/promises and a YAML lib
(e.g., js-yaml via import('js-yaml')), read the file at the existing
possiblePaths loop, parse the file with yaml.load (or safeLoad) into a
Record<string, any>, and return that object; keep the existing possiblePaths
logic and error handling around the read/parse attempts and ensure you handle
both absolute/relative paths and parsing errors.

54-89: ⚠️ Potential issue | 🔴 Critical

Layer coverage parsing is structurally incorrect and yields empty layers.

Line 62 captures only spacing/asterisk content, so Lines 76–83 never see L0L5 tokens; additionally, multi-word agent names and dash placeholders are not handled. This causes incorrect evolution stage derivation downstream.

💡 Proposed fix (parse markdown table columns explicitly)
 function parseLayerCoverage(markdown: string): Map<string, string[]> {
   const layerSection = markdown.split('### Agent Layer Coverage Map')[1]?.split('###')[0];
   if (!layerSection) return new Map();

-  const agentLayers = new Map();
-  const lines = layerSection.split('\n');
-
-  for (const line of lines) {
-    const match = line.match(/([A-Z][A-Za-z-]+)\s+\*+\s+([\s\*]+)\s+(\d+)/);
-    if (match) {
-      const name = match[1];
-      const layerStr = match[2];
-      const layerCountStr = match[3];
-      const layerCount = parseInt(layerCountStr, 10); // Parsed but unused; we use layerStr parsing instead
-
-      // Validate layerCount is a valid number (defensive programming)
-      if (!isNaN(layerCount)) {
-        const _layerCount = layerCount;
-      }
-
-      const layers: string[] = [];
-      if (layerStr.includes('*')) {
-        if (layerStr.includes('L0')) layers.push('L0');
-        if (layerStr.includes('L1')) layers.push('L1');
-        if (layerStr.includes('L2')) layers.push('L2');
-        if (layerStr.includes('L2.5')) layers.push('L2.5');
-        if (layerStr.includes('L3')) layers.push('L3');
-        if (layerStr.includes('L4')) layers.push('L4');
-        if (layerStr.includes('L5')) layers.push('L5');
-      }
-
-      agentLayers.set(name, layers);
-    }
-  }
+  const agentLayers = new Map<string, string[]>();
+  const rows = layerSection
+    .split('\n')
+    .map((line) => line.trim())
+    .filter((line) => line.startsWith('|') && line.endsWith('|'));
+
+  if (rows.length < 3) return agentLayers;
+
+  const headers = rows[0].split('|').slice(1, -1).map((c) => c.trim());
+  const supportedLayers = ['L0', 'L1', 'L2', 'L2.5', 'L3', 'L4', 'L5'] as const;
+
+  for (const row of rows.slice(2)) { // skip header + separator
+    const cells = row.split('|').slice(1, -1).map((c) => c.trim());
+    const name = cells[0];
+    if (!name) continue;
+
+    const layers: string[] = [];
+    for (const layer of supportedLayers) {
+      const idx = headers.indexOf(layer);
+      if (idx === -1) continue;
+      const marker = cells[idx] ?? '';
+      if (marker.includes('*') || marker.toLowerCase() === 'x' || marker === '✅') {
+        layers.push(layer);
+      }
+    }
+
+    agentLayers.set(name, layers);
+  }

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

In `@pmoves/ui/app/api/agents/taxonomy/route.ts` around lines 54 - 89, The
parseLayerCoverage function currently captures the wrong column (only
spaces/asterisks) and so never finds L0–L5; update parseLayerCoverage to parse
the markdown table columns explicitly instead of the current regex — for each
non-header row split on '|' (or use a regex that captures three table columns),
trim columns to extract the agent name (support multi-word names and '-'
placeholders), the layer-markers column, and the count column; then scan the
layer-markers text for tokens 'L0','L1','L2','L2.5','L3','L4','L5' and populate
the layers array accordingly before calling agentLayers.set(name, layers).
Ensure you still parse/validate the numeric count (layerCount) if needed and
preserve the function signature parseLayerCoverage(markdown: string):
Map<string,string[]> and variable names used (agentLayers, layerSection, lines).
pmoves/ui/app/api/github/prs/route.ts (5)

93-100: ⚠️ Potential issue | 🟠 Major

Resolve the Archon base URL from env here too.

The codebase already has env-based Archon resolution in pmoves/ui/lib/api/archon.ts; hard-coding http://localhost:8091 here means any deployment where Archon is not co-located will fall straight into the 503 branch.

Proposed fix
 async function getGitHubToken(): Promise<string | null> {
   // Try to get token from Archon service which has minting capability
   try {
-    const archonResponse = await fetch('http://localhost:8091/api/github/token', {
+    const archonBaseUrl = (
+      process.env.NEXT_PUBLIC_ARCHON_URL ||
+      process.env.ARCHON_URL ||
+      'http://localhost:8091'
+    ).replace(/\/$/, '');
+
+    const archonResponse = await fetch(new URL('/api/github/token', archonBaseUrl), {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       signal: AbortSignal.timeout(5000),
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 93 - 100, The
getGitHubToken function currently hard-codes Archon as 'http://localhost:8091';
update it to resolve Archon's base URL from the same environment-aware helper
used elsewhere (referencing the resolution logic in pmoves/ui/lib/api/archon.ts)
and compose the token endpoint using that base URL instead of the literal string
so deployments that set ARCHON_* env vars or alternate hosts will call the
correct service; ensure the fetch still uses POST, JSON headers, and the
existing AbortSignal.timeout(5000).

124-124: ⚠️ Potential issue | 🟠 Major

Validate or encode repo before using it in the GitHub request path.

Line 151 forwards the raw query value into fetchRepoPRs, and that value is later interpolated into the GitHub URL path. A value containing /, .., or ? can change which GitHub resource gets requested with the installation token, and bad names currently degrade into an empty 200 instead of a 400.

Also applies to: 151-152

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

In `@pmoves/ui/app/api/github/prs/route.ts` at line 124, The repo query value
obtained by const repoFilter = searchParams.get('repo') is forwarded into
fetchRepoPRs and later interpolated into the GitHub request path; validate
and/or encode it before use by checking it matches the expected owner/repo
pattern (e.g., two path segments of allowed characters), reject anything with
path traversal, slashes in segments, or disallowed characters with a 400 Bad
Request, or alternatively URL-encode each path segment before interpolating;
update the code path that calls fetchRepoPRs (and fetchRepoPRs itself if
necessary) to perform this validation/encoding so raw user input is never
directly inserted into the GitHub URL.

125-127: ⚠️ Potential issue | 🟡 Minor

Clamp limit to a positive integer too.

The NaN fallback is fixed, but 0 and negatives still get through. ?limit=-5 becomes .slice(0, -5) and drops the last five items instead of defaulting to 50.

Proposed fix
-  const limitParsed = parseInt(limitParam || '50', 10);
-  const limit = isNaN(limitParsed) ? 50 : Math.min(limitParsed, 100);
+  const limitParsed = Number.parseInt(limitParam ?? '', 10);
+  const limit =
+    Number.isFinite(limitParsed) && limitParsed > 0
+      ? Math.min(limitParsed, 100)
+      : 50;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 125 - 127, The current
parsing of query param limit (variables limitParam, limitParsed, limit) handles
NaN but allows 0 and negatives which cause incorrect slicing; update the logic
to treat any non-positive values as the default (50) and otherwise clamp to a
maximum of 100 — i.e., after parseInt, if limitParsed is NaN or less than 1 set
limit to 50, else set limit to the lesser of limitParsed and 100 so slicing
never receives 0 or a negative end.

137-145: ⚠️ Potential issue | 🟡 Minor

Keep the 503 payload counters consistent with the other branches.

This branch still omits open, merged, closed, and draft, even though the success path and the 500 path include them. Callers now have to special-case the token-failure response for the same counters.

Proposed fix
         {
           error: 'GitHub token unavailable',
           message: 'Archon service unavailable or GitHub App not configured',
           items: [],
           timestamp: new Date().toISOString(),
           total: 0,
+          open: 0,
+          merged: 0,
+          closed: 0,
+          draft: 0,
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 137 - 145, The 503
early-return when token is missing omits the PR counters (open, merged, closed,
draft) that other branches include; inside the token check block in route.ts
(the if (!token) return NextResponse.json(...) branch) add the same counter
fields (open, merged, closed, draft) with zero values to the JSON payload so the
response shape matches the success and 500 paths, keeping timestamp, total and
items as currently set.

21-33: ⚠️ Potential issue | 🟠 Major

Normalize merged/draft state after the REST fetch.

GitHub's List pull requests endpoint only accepts state=open|closed|all. Right now this route can send MERGED, and mapState() never upgrades pr.merged_at / pr.draft into PRStatus.state, so ?state=MERGED and ?state=DRAFT return the wrong set, Line 164 counts drafts as open, and Line 165 can never increment. (docs.github.com)

Also applies to: 42-68, 163-167

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pmoves/ui/app/api/agents/taxonomy/route.ts`:
- Around line 215-267: The custom line-by-line parser in loadSignatures
incorrectly expects '-' list items and misses normal YAML mappings, resulting in
empty signatures; replace the heuristic parsing with a proper YAML parser:
import fs/promises and a YAML lib (e.g., js-yaml via import('js-yaml')), read
the file at the existing possiblePaths loop, parse the file with yaml.load (or
safeLoad) into a Record<string, any>, and return that object; keep the existing
possiblePaths logic and error handling around the read/parse attempts and ensure
you handle both absolute/relative paths and parsing errors.
- Around line 54-89: The parseLayerCoverage function currently captures the
wrong column (only spaces/asterisks) and so never finds L0–L5; update
parseLayerCoverage to parse the markdown table columns explicitly instead of the
current regex — for each non-header row split on '|' (or use a regex that
captures three table columns), trim columns to extract the agent name (support
multi-word names and '-' placeholders), the layer-markers column, and the count
column; then scan the layer-markers text for tokens
'L0','L1','L2','L2.5','L3','L4','L5' and populate the layers array accordingly
before calling agentLayers.set(name, layers). Ensure you still parse/validate
the numeric count (layerCount) if needed and preserve the function signature
parseLayerCoverage(markdown: string): Map<string,string[]> and variable names
used (agentLayers, layerSection, lines).

In `@pmoves/ui/app/api/github/prs/route.ts`:
- Around line 93-100: The getGitHubToken function currently hard-codes Archon as
'http://localhost:8091'; update it to resolve Archon's base URL from the same
environment-aware helper used elsewhere (referencing the resolution logic in
pmoves/ui/lib/api/archon.ts) and compose the token endpoint using that base URL
instead of the literal string so deployments that set ARCHON_* env vars or
alternate hosts will call the correct service; ensure the fetch still uses POST,
JSON headers, and the existing AbortSignal.timeout(5000).
- Line 124: The repo query value obtained by const repoFilter =
searchParams.get('repo') is forwarded into fetchRepoPRs and later interpolated
into the GitHub request path; validate and/or encode it before use by checking
it matches the expected owner/repo pattern (e.g., two path segments of allowed
characters), reject anything with path traversal, slashes in segments, or
disallowed characters with a 400 Bad Request, or alternatively URL-encode each
path segment before interpolating; update the code path that calls fetchRepoPRs
(and fetchRepoPRs itself if necessary) to perform this validation/encoding so
raw user input is never directly inserted into the GitHub URL.
- Around line 125-127: The current parsing of query param limit (variables
limitParam, limitParsed, limit) handles NaN but allows 0 and negatives which
cause incorrect slicing; update the logic to treat any non-positive values as
the default (50) and otherwise clamp to a maximum of 100 — i.e., after parseInt,
if limitParsed is NaN or less than 1 set limit to 50, else set limit to the
lesser of limitParsed and 100 so slicing never receives 0 or a negative end.
- Around line 137-145: The 503 early-return when token is missing omits the PR
counters (open, merged, closed, draft) that other branches include; inside the
token check block in route.ts (the if (!token) return NextResponse.json(...)
branch) add the same counter fields (open, merged, closed, draft) with zero
values to the JSON payload so the response shape matches the success and 500
paths, keeping timestamp, total and items as currently set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e04497f-2169-42ab-a79f-f476be85a334

📥 Commits

Reviewing files that changed from the base of the PR and between 5cb1ecc and e792296.

📒 Files selected for processing (3)
  • pmoves/ui/app/api/agents/taxonomy/route.ts
  • pmoves/ui/app/api/github/prs/route.ts
  • pmoves/ui/app/api/graphiti/trails/route.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • pmoves/ui/app/api/graphiti/trails/route.ts

POWERFULMOVES added a commit that referenced this pull request Mar 14, 2026
Remove all API route, UI component, and E2E test changes that overlap
with PR #922 (feat/pmovesui-api-routes). This PR now contains only:

- GEOMETRY BUS integration documentation (8 Mermaid diagrams, NATS
  subject taxonomy, service guides, pipeline docs, CGP schema refs)
- CONCH integration map
- UI .gitignore: add test-results/ and playwright-report/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@POWERFULMOVES
POWERFULMOVES force-pushed the feat/pmovesui-api-routes branch from e792296 to ea7c4fc Compare March 14, 2026 21:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

♻️ Duplicate comments (6)
pmoves/ui/app/api/github/prs/route.ts (4)

138-145: ⚠️ Potential issue | 🟡 Minor

Keep 503 response shape consistent with PRListResponse.

This branch omits open, merged, closed, and draft, which makes response schema inconsistent across outcomes.

Suggested fix
         {
           error: 'GitHub token unavailable',
           message: 'Archon service unavailable or GitHub App not configured',
           items: [],
           timestamp: new Date().toISOString(),
           total: 0,
+          open: 0,
+          merged: 0,
+          closed: 0,
+          draft: 0,
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 138 - 145, The 503 JSON
response returned in route.ts (the NextResponse.json call when GitHub token is
unavailable) is missing fields required by PRListResponse; update that response
object to include open, merged, closed, and draft with appropriate default
values (e.g., numeric counts set to 0 and boolean draft as false or an empty
list per the PRListResponse shape) so the error payload matches the
PRListResponse schema and downstream consumers don't break.

126-127: ⚠️ Potential issue | 🟡 Minor

Clamp limit to a positive range before slicing.

Negative values survive current parsing and produce unexpected slice(0, negative) behavior.

Suggested fix
-  const limitParsed = parseInt(limitParam || '50', 10);
-  const limit = isNaN(limitParsed) ? 50 : Math.min(limitParsed, 100);
+  const limitParsed = Number.parseInt(limitParam ?? '', 10);
+  const limit = Number.isFinite(limitParsed)
+    ? Math.min(Math.max(limitParsed, 1), 100)
+    : 50;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 126 - 127, The current
parsing sets limit via limitParsed/limit but allows negative values, causing
invalid slice(0, limit) behavior; update the logic around limitParam,
limitParsed and limit in route.ts to clamp to a positive range (e.g., treat
negatives as 0 or the default 50) before computing Math.min and before any slice
call — ensure limit is coerced to an integer >= 0 and <= 100 so downstream
slice(0, limit) behaves correctly.

9-10: ⚠️ Potential issue | 🟠 Major

Edge runtime + hard-coded localhost token endpoint is brittle in deployment.

With runtime = 'edge', fetching http://localhost:8091/... will usually fail outside local dev, causing persistent 503 responses.

For Next.js route handlers using runtime='edge', can requests to http://localhost:<port> reach local services in production deployments?

Also applies to: 96-100

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

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 9 - 10, The route is set
to runtime='edge' (export const runtime) while the handler fetches a hard-coded
http://localhost:8091/... endpoint, which will break outside local dev; change
the fetch to use a configurable, environment-driven URL (e.g.,
process.env.TOKEN_ENDPOINT or NEXT_PUBLIC_TOKEN_ENDPOINT) or a relative/internal
API route so the endpoint resolves in deployed edge environments, and ensure the
code that references that URL (the route handler function that performs the
fetch) uses the new env variable or proxy path instead of the literal
localhost:8091.

44-47: ⚠️ Potential issue | 🟠 Major

PR state mapping/filtering is inconsistent with GitHub Pulls API semantics.

state=MERGED is not valid for the pulls list API, and merged/draft state is not derived from merged_at / draft before stats/filtering, so merged counts and filtering can be wrong.

GitHub REST API "List pull requests" endpoint: what are the allowed values for the "state" query parameter, and how should merged/draft PRs be derived from response fields?

Also applies to: 68-69, 123-131, 163-167

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

In `@pmoves/ui/app/api/github/prs/route.ts` around lines 44 - 47, The pulls list
API only accepts state values "open", "closed" or "all" (lowercase) so change
the stateFilter computation (symbol: stateFilter) to produce one of those three
values and use that in the URL construction, then post-process the returned PR
objects to derive merged and draft status from the response fields (check
pr.merged_at !== null for merged, and pr.draft === true for draft) when
computing counts/filters; update any downstream filtering/aggregation logic that
assumed state=MERGED (refer to the URL construction and the code blocks around
stateFilter and the later places that build counts/filters at the mentioned
locations) so merged/draft decisions are based on pr.merged_at and pr.draft
instead of the request query parameter.
pmoves/ui/app/api/agents/taxonomy/route.ts (2)

233-264: ⚠️ Potential issue | 🔴 Critical

loadSignatures() parser logic still drops normal YAML structures.

The parser only processes lines starting with - (Line 234), so top-level and nested YAML mappings are skipped, resulting in empty/incomplete signatures.

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

In `@pmoves/ui/app/api/agents/taxonomy/route.ts` around lines 233 - 264, The
current line-by-line parser in loadSignatures (the loop over content.split('\n')
using indentMatch = line.match(/^(\s*)-/)) only processes lines that start with
'-' and thus skips normal YAML mappings; replace this fragile ad-hoc parsing
with a proper YAML parse: import and call a YAML loader (e.g., js-yaml's load)
on content, validate/normalize the resulting object into the signatures
structure, and assign signatures[currentSection] from the parsed object instead
of relying on indentMatch/currentSection logic; if you must keep manual parsing,
remove the /^(\s*)-/ check and instead detect top-level section keys (keyMatch)
and nested key/value pairs robustly (handle arrays/booleans) so that mappings
like "section:" and indented "key: value" are not ignored.

61-86: ⚠️ Potential issue | 🔴 Critical

Layer coverage parsing is still non-functional for taxonomy data.

The current matcher in Line 62 does not handle multi-word agent names, and layerStr in Line 65 cannot contain L0..L5, so the checks in Line 76–82 never populate layers correctly.

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

In `@pmoves/ui/app/api/agents/taxonomy/route.ts` around lines 61 - 86, The regex
and layer parsing are wrong: the matcher for each line (variable match) doesn't
allow multi-word agent names and layerStr currently cannot contain L0..L5 so the
subsequent checks never find layers. Update the regex used when iterating lines
to allow multi-word names and to capture a layer token string that can include
tokens like L0, L1, L2, L2.5 etc (e.g. make the name group non-greedy and the
layer group allow letters/digits/dot like
/(.*?)\s+\*+\s+([A-Za-z0-9\.\s\*L+-]+)\s+(\d+)/ or similar), then replace the
manual if(layerStr.includes('Lx')) checks with a robust extract using a global
match for /L2\.5|L[0-5]/g to populate the layers array, and either remove the
unused parsed layerCount (layerCount/ layerCountStr) or actually use it; ensure
you still set agentLayers.set(name, layers).
🟠 Major comments (21)
pmoves/ui/app/dashboard/research/page.tsx-67-69 (1)

67-69: ⚠️ Potential issue | 🟠 Major

Keep selectedTask synchronized after polled task updates.

Line 68 updates only tasks. If the selected task changes status (e.g., runningcompleted), the details pane can remain stale because it renders from selectedTask, not from tasks.

Suggested fix
+  const applyTaskSnapshot = useCallback((nextTasks: ResearchTask[]) => {
+    setTasks(nextTasks);
+    setSelectedTask(prev =>
+      prev ? nextTasks.find(t => t.id === prev.id) ?? null : null
+    );
+  }, []);

   // Initial load
   const initialLoad = async () => {
     setRefreshing(true);
     const taskResult = await listResearchTasks({ limit: 50 });
     if (taskResult.ok) {
-      setTasks(taskResult.data);
+      applyTaskSnapshot(taskResult.data);
     } else {
       setError(taskResult.error);
       setTimeout(() => setError(null), 5000);
     }

   // Poll branch
   listResearchTasks({ limit: 50 })
     .then(result => {
       if (result.ok) {
-        setTasks(result.data);
+        applyTaskSnapshot(result.data);
       }
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/dashboard/research/page.tsx` around lines 67 - 69, After
updating tasks in the poll handler, also synchronize the currently selected
task: use the existing setSelectedTask callback to look up the updated task in
result.data by matching the selectedTask.id and replace selectedTask with the
fresh object (or keep the previous selectedTask if it no longer exists), so the
details pane reflects status/field changes; update the code near
setTasks(result.data) and use setSelectedTask(prev => { find matching task in
result.data by id; return matchingTask ?? prev; }) referencing setTasks,
setSelectedTask, selectedTask, and result.data.
pmoves/ui/app/dashboard/research/page.tsx-61-73 (1)

61-73: ⚠️ Potential issue | 🟠 Major

Prevent overlapping poll requests.

Line 61 starts polling every 5s, but listResearchTasks can run up to 10s (see pmoves/ui/lib/api/research.ts:236-279), so multiple in-flight polls can overlap and race on setTasks.

Suggested fix
   const tasksRef = useRef<ResearchTask[]>([]);
+  const pollInFlightRef = useRef(false);

   // Poll for updates on running tasks - uses ref to avoid dependency on tasks state
   const interval = setInterval(() => {
     const hasRunning = tasksRef.current.some(t => t.status === "running");
-    if (hasRunning) {
+    if (hasRunning && !pollInFlightRef.current) {
+      pollInFlightRef.current = true;
       setRefreshing(true);
       listResearchTasks({ limit: 50 })
         .then(result => {
           if (result.ok) {
             setTasks(result.data);
           }
         })
-        .finally(() => setRefreshing(false));
+        .finally(() => {
+          pollInFlightRef.current = false;
+          setRefreshing(false);
+        });
     }
   }, 5000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/dashboard/research/page.tsx` around lines 61 - 73, The polling
loop can start a new listResearchTasks call before the previous one finishes,
causing overlapping requests and racey setTasks; add an in-flight guard (e.g., a
useRef boolean like pollingRef or isFetchingRef) checked at the top of the
interval callback and return early if true, set it to true immediately before
invoking listResearchTasks({ limit: 50 }) and set it back to false in the
.finally() where setRefreshing(false) is called; update references to tasksRef,
listResearchTasks, setRefreshing, and setTasks accordingly so only one request
is active at a time.
pmoves/ui/e2e/research.spec.ts-239-239 (1)

239-239: ⚠️ Potential issue | 🟠 Major

Test has no assertions validating pending task selection.

The test "should select only pending tasks" clicks the selection button but only asserts that the button is visible. Line 239 creates a locator without using it, leaving the core functionality unverified. The test will pass even if the button doesn't select any tasks.

The test needs assertions to verify:

  • Pending tasks are actually selected
  • Non-pending tasks remain unselected
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/research.spec.ts` at line 239, The test "should select only
pending tasks" currently creates a locator but never asserts selection; update
the test to (1) click the selection button as already done, (2) locate pending
items using page.locator('[data-testid="task-item"][data-status="pending"]') and
assert each pending item shows the selection state (e.g. contains the selection
class/attribute your app uses such as a 'selected' class,
aria-selected/aria-checked, or a checked input), and (3) locate non-pending
items with
page.locator('[data-testid="task-item"]:not([data-status="pending"])') and
assert none of those show the selection state; use the existing selection button
and these locators to validate that only pending tasks became selected.
pmoves/ui/e2e/research.spec.ts-81-81 (1)

81-81: ⚠️ Potential issue | 🟠 Major

Add missing assertions to validate default-collapsed and collapse behavior.

Line 81 captures the initial visibility state but never asserts that the panel is collapsed by default. Line 93 captures the post-collapse visibility state but never asserts that the collapse succeeded. The expand behavior is properly validated on line 87, but the collapse and initial default state lack assertions. This allows the test to pass even if those behaviors regress.

Proposed fix
-    const _isInitiallyVisible = await optionsPanel.isVisible().catch(() => false);
+    const isInitiallyVisible = await optionsPanel.isVisible().catch(() => false);
+    expect(isInitiallyVisible).toBe(false);
@@
-    const _isCollapsed = await optionsPanel.isVisible({ timeout: 1000 }).catch(() => false);
+    const isCollapsed = await optionsPanel.isVisible({ timeout: 1000 }).catch(() => false);
+    expect(isCollapsed).toBe(false);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/research.spec.ts` at line 81, The test captures optionsPanel
visibility into _isInitiallyVisible but never asserts it; update the test to
assert the panel is collapsed by default by asserting that the result of
optionsPanel.isVisible() (the _isInitiallyVisible value) is false. Similarly,
after triggering the collapse action (the code capturing the post-collapse
visibility around line 93), add an assertion that that visibility value is false
to ensure collapse succeeded. Locate uses of optionsPanel.isVisible() and the
post-collapse visibility variable and add expect(...).toBe(false) (or the
project's equivalent assertion) immediately after those captures.
pmoves/ui/e2e/research.spec.ts-24-24 (1)

24-24: ⚠️ Potential issue | 🟠 Major

Remove floating async call that cannot fail the test.

Line 24 calls isVisible().catch(() => false) without await, so the promise never completes and the result is discarded. This test step cannot fail when the element is missing.

Proposed fix
-    page.locator('[data-testid="research-results"]').isVisible().catch(() => false);
+    await expect(page.locator('[data-testid="research-results"]')).toBeVisible();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/research.spec.ts` at line 24, The floating promise
page.locator('[data-testid="research-results"]').isVisible().catch(() => false)
is not awaited so it can't fail the test; replace it with an awaited assertion —
e.g., await
expect(page.locator('[data-testid="research-results"]')).toBeVisible() (or await
page.locator(...).isVisible() with an explicit assert) so the visibility check
completes and can fail the test. Make this change where the locator call
appears.
pmoves/ui/e2e/research.spec.ts-183-185 (1)

183-185: ⚠️ Potential issue | 🟠 Major

The test can pass even when the dropdown is missing.

The isVisible().catch(() => false) pattern silently masks missing elements and gates all assertions behind the if (_exists) block. For a test named "should select notebook from dropdown", the element must be present to pass.

Replace the try-catch pattern with a direct assertion:

Fix
-    const _exists = await notebookSelect.isVisible({ timeout: 1000 }).catch(() => false);
-
-    if (_exists) {
+    await expect(notebookSelect).toBeVisible({ timeout: 1000 });
       // Select a notebook
       await page.selectOption('[data-testid="notebook-select"]', { index: 0 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/research.spec.ts` around lines 183 - 185, The test currently
masks a missing dropdown by using notebookSelect.isVisible({ timeout: 1000
}).catch(() => false) and conditionally running the assertions inside if
(_exists); remove that try/catch and the if (_exists) guard and replace with a
direct assertion that the dropdown is visible (e.g., assert/expect the result of
notebookSelect.isVisible or call an assertion helper) in the "should select
notebook from dropdown" test so the test fails when the element is absent;
update references to notebookSelect and the visibility check accordingly.
pmoves/ui/app/api/services/health-enhanced/route.ts-16-24 (1)

16-24: ⚠️ Potential issue | 🟠 Major

llm is mapped to two tiers, which breaks tier consistency.

With Line 18 and Line 19 both containing llm, Line 137’s first-match logic always resolves llm services to Tier 2. Tier 3 stats/filtering become inconsistent.

🔧 Proposed fix (make category→tier mapping unique)
 const TIER_CATEGORIES: Record<number, string[]> = {
-  2: ['api', 'bus', 'llm'],
+  2: ['api', 'bus'],
 };
-      let tier = 4; // default
-      for (const [t, categories] of Object.entries(TIER_CATEGORIES)) {
-        if (categories.includes(service.category)) {
-          tier = parseInt(t, 10);
-          break;
-        }
-      }
+      const matchedTier = Object.entries(TIER_CATEGORIES).find(([, categories]) =>
+        categories.includes(service.category)
+      )?.[0];
+      const tier = matchedTier ? Number(matchedTier) : 4;

Also applies to: 137-140

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

In `@pmoves/ui/app/api/services/health-enhanced/route.ts` around lines 16 - 24,
TIER_CATEGORIES contains the category "llm" in both tier 2 and tier 3 which
causes the first-match tier resolution logic that uses TIER_CATEGORIES to always
assign "llm" to tier 2 and break tier 3 stats; remove the duplicate by keeping
"llm" in only one tier (e.g., remove "llm" from the array in TIER_CATEGORIES for
tier 3) and then update any tier-resolution code that iterates TIER_CATEGORIES
(the same mapping and the first-match logic) to rely on the now-unique
category→tier mapping; optionally add a small uniqueness check/assert after
building the reverse map to fail fast if duplicates reappear.
pmoves/ui/app/api/services/health-enhanced/route.ts-107-125 (1)

107-125: ⚠️ Potential issue | 🟠 Major

Scope probes to filtered services and validate tier before probing.

Line 108 accepts non-numeric/out-of-range tier, and Line 122 still probes broadly. This allows malformed requests to trigger unnecessary checks against all services.

🔧 Proposed fix
-    if (tierParam) {
-      const tier = parseInt(tierParam, 10);
-      const categories = TIER_CATEGORIES[tier] || [];
+    let parsedTier: number | undefined;
+    if (tierParam) {
+      parsedTier = Number.parseInt(tierParam, 10);
+      if (!Number.isInteger(parsedTier) || parsedTier < 1 || parsedTier > 7) {
+        return NextResponse.json({ error: 'Invalid tier. Use 1-7.' }, { status: 400 });
+      }
+      const categories = TIER_CATEGORIES[parsedTier] || [];
       servicesToCheck = servicesToCheck.filter((s) => categories.includes(s.category));
     }
@@
-    const healthResult: HealthCheckResult = await checkAllServices(
-      timeout,
-      categoryParam ? { category: categoryParam as any } : undefined
-    );
+    const healthResult: HealthCheckResult = await checkAllServices(timeout, {
+      ...(categoryParam ? { category: categoryParam } : {}),
+      slugs: servicesToCheck.map((s) => s.slug),
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/services/health-enhanced/route.ts` around lines 107 - 125,
Validate tierParam before using it by ensuring parseInt(tierParam, 10) yields a
number and that the resulting tier exists in TIER_CATEGORIES; if invalid, reject
or ignore the tierParam. After applying filters to servicesToCheck (using
tierParam/TIER_CATEGORIES, categoryParam, and agentsOnly), pass that filtered
servicesToCheck into checkAllServices so probes are limited to the scoped list
(replace the existing call that only passes timeout and category); reference the
symbols tierParam, TIER_CATEGORIES, servicesToCheck, checkAllServices, and
HealthCheckResult when locating the changes.
pmoves/ui/app/api/services/health-enhanced/route.ts-219-222 (1)

219-222: ⚠️ Potential issue | 🟠 Major

Do not expose raw internal error messages to clients.

Line 221 returns error.message directly, which can leak internal hostnames/topology from probe failures.

🔧 Proposed fix
   } catch (error) {
+    console.error('Enhanced health check failed', error);
     return NextResponse.json(
       {
         error: 'Enhanced health check failed',
-        message: error instanceof Error ? error.message : String(error),
+        message: 'Internal server error',
         services: [],
         tiers: {},
         agents: { total: 0, healthy: 0, unhealthy: 0 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/services/health-enhanced/route.ts` around lines 219 - 222,
The response currently returns raw error details to clients (message: error
instanceof Error ? error.message : String(error)) which can leak internal info;
change the response to return a generic client-safe message (e.g. "Internal
health probe error") instead of error.message, and move the actual error logging
to server logs (use existing logger or console.error) within the same handler in
route.ts where the response object with error: 'Enhanced health check failed' is
created so clients only see the generic message while full error details remain
in server logs.
pmoves/ui/lib/constants/errorIds.ts-84-90 (1)

84-90: ⚠️ Potential issue | 🟠 Major

Multiple semantic mismatches in Archon error ID usage.

The Archon API client uses ARCHON_PROMPT_LIST_FAILED incorrectly in three places:

  1. archonGetPrompt (lines 266, 279) — logs "get-prompt" operations with LIST error ID
  2. archonListForms (line 529) — logs form operations with prompt error ID
  3. archonGetForm (line 564) — logs form get operations with prompt error ID

This reduces monitoring accuracy by conflating different operation types and resources. Add dedicated error IDs for get operations and form-related failures:

🔧 Suggested fix
   // === ARCHON ERRORS ===
   ARCHON_PROMPT_LIST_FAILED: 'archon_prompt_list_failed',
+  ARCHON_PROMPT_GET_FAILED: 'archon_prompt_get_failed',
   ARCHON_PROMPT_CREATE_FAILED: 'archon_prompt_create_failed',
   ARCHON_PROMPT_UPDATE_FAILED: 'archon_prompt_update_failed',
   ARCHON_PROMPT_DELETE_FAILED: 'archon_prompt_delete_failed',
   ARCHON_PROMPT_EXECUTE_FAILED: 'archon_prompt_execute_failed',
   ARCHON_HEALTH_CHECK_FAILED: 'archon_health_check_failed',
+
+  // === ARCHON FORM ERRORS ===
+  ARCHON_FORM_LIST_FAILED: 'archon_form_list_failed',
+  ARCHON_FORM_GET_FAILED: 'archon_form_get_failed',

Then update the three error logging calls to use the appropriate error IDs.

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

In `@pmoves/ui/lib/constants/errorIds.ts` around lines 84 - 90, Add distinct error
IDs for prompt GET and form operations and update the Archon call sites to use
them: define new constants (e.g., ARCHON_PROMPT_GET_FAILED,
ARCHON_FORM_LIST_FAILED, ARCHON_FORM_GET_FAILED) alongside the existing ARCHON_*
IDs in errorIds.ts, then replace usages of ARCHON_PROMPT_LIST_FAILED in the
archonGetPrompt function (both log calls around lines where archonGetPrompt logs
"get-prompt"), replace the prompt ID in archonListForms with
ARCHON_FORM_LIST_FAILED, and replace the prompt ID in archonGetForm with
ARCHON_FORM_GET_FAILED so each operation (prompt get, form list, form get) logs
its own specific error id.
pmoves/ui/e2e/ingestion.spec.ts-102-106 (1)

102-106: ⚠️ Potential issue | 🟠 Major

The “select pending” flow stopped verifying anything.

await pendingItems.count() discards the only observable state here, so this test still passes if the button selects nothing or selects non-pending rows. Assert the checked state or selected-count text after the click.

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

In `@pmoves/ui/e2e/ingestion.spec.ts` around lines 102 - 106, The test currently
calls pendingItems.count() without asserting its value and only checks
visibility of the select button, so it doesn't verify selection behavior; update
the test around the pendingItems locator and the
'[data-testid="select-pending"]' click to assert that selection actually
occurred by (1) capturing the number of pending rows via pendingItems.count()
and asserting it's >0, (2) clicking
page.locator('[data-testid="select-pending"]'), and then (3) asserting selection
state either by checking a selected indicator on rows (e.g., rows with
'[data-testid="queue-item"][data-status="pending"][aria-checked="true"]' or a
checked class) or the selected-count text element to equal the prior pending
count; use the existing pendingItems and select-pending locators to find and
verify the selected items.
pmoves/ui/e2e/archon-prompts.spec.ts-171-173 (1)

171-173: ⚠️ Potential issue | 🟠 Major

Avoid hard-coding /dashboard/archon-prompts/test-prompt in E2E flows.

These suites assume a prompt with slug test-prompt exists in every environment. If it doesn't, edit/delete/execute coverage degrades into 404s or silently skipped assertions. Navigate from a created/listed prompt or seed the fixture explicitly.

Also applies to: 203-203, 220-220, 241-253

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

In `@pmoves/ui/e2e/archon-prompts.spec.ts` around lines 171 - 173, The E2E test
'saves prompt changes' (and other tests in archon-prompts.spec.ts) hard-codes
the slug '/dashboard/archon-prompts/test-prompt', which can 404; change the flow
to create or select a real prompt at runtime: either create a prompt fixture via
the app/API before navigation (use a POST/setup helper) and then navigate to its
returned slug, or first go to the prompts listing page and click into the
first/newly created prompt to obtain a valid route; update the tests (e.g., the
test function named "saves prompt changes" and the other affected tests at the
referenced ranges) to use the created/listed prompt slug instead of the fixed
'/dashboard/archon-prompts/test-prompt'.
pmoves/ui/app/dashboard/jellyfin/page.tsx-147-149 (1)

147-149: ⚠️ Potential issue | 🟠 Major

The new “Link” button is only a console log.

This adds a user-facing action that never links anything, so search results look interactive but do nothing. Please wire it to the real linking flow or remove/disable the button until that exists.

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

In `@pmoves/ui/app/dashboard/jellyfin/page.tsx` around lines 147 - 149, The "Link"
button currently only logs to console; either wire it to the real linking flow
or disable it until that flow exists. Replace the inline onClick={() =>
console.log("Link item:", item.id)} on the button with a call to the actual
linking handler (e.g., handleLinkClick or an existing linkMediaItem(item.id) /
openLinkModal(item) function) so it triggers the linking flow, or remove the
onClick and add disabled={true} plus aria-disabled and a tooltip to indicate the
feature is not available; update any referenced handler names (handleLinkClick,
linkMediaItem, openLinkModal, item.id) accordingly.
pmoves/ui/e2e/archon-prompts.spec.ts-71-74 (1)

71-74: ⚠️ Potential issue | 🟠 Major

Several of these assertions are false positives.

expect(... || true).toBe(true) can never fail, and _hasError is computed but never asserted. As written, these paths won't catch regressions in filtering, search, create validation, or save behavior.

Also applies to: 89-91, 136-142, 190-195

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

In `@pmoves/ui/e2e/archon-prompts.spec.ts` around lines 71 - 74, The assertions
using "expect(... || true).toBe(true)" are false positives and should be
replaced with real checks; in archon-prompts.spec.ts locate the places computing
url and hasCategoryInUrl (and the computed but unused _hasError) and change them
to assert the actual expected condition (e.g.,
expect(hasCategoryInUrl).toBe(true) or assert the page content change), and for
validation/save tests assert the computed _hasError (or its UI indicator) with
expect(_hasError).toBe(true/false) as appropriate so the tests fail on
regressions instead of always passing.
pmoves/ui/e2e/chat.spec.ts-125-134 (1)

125-134: ⚠️ Potential issue | 🟠 Major

These new flows don't verify the behavior they claim to cover.

The failed-request case never forces a chat API error, and the clear-history case clicks through the UI without asserting that the transcript changed. Both tests can stay green while error handling or history clearing is broken.

Also applies to: 153-165

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

In `@pmoves/ui/e2e/chat.spec.ts` around lines 125 - 134, The tests "shows error
message on failed request" and the clear-history flow are not actually forcing
or asserting the behaviors they claim; update the error test (test 'shows error
message on failed request') to intercept the chat API request (use page.route or
network mocking for the chat endpoint) and respond with a 500/error payload,
then perform the UI action that triggers the request and assert that the locator
'[class*="error"], [role="alert"]' becomes visible; for the clear-history test
(lines ~153-165) capture the transcript content before clicking the
clear/history button, perform the click that triggers clear (use the same
selector used in the test), and assert the transcript has been removed or
replaced (e.g., empty or different text) to verify the history was actually
cleared.
pmoves/ui/app/api/graphiti/trails/route.ts-67-84 (1)

67-84: ⚠️ Potential issue | 🟠 Major

verifiedOnly is filtering on signature presence, not signature validity.

isVerified is derived from !!raw.sig, and signatureValid is hard-coded to true whenever a signature exists. That means any entry with a sig field is counted as verified and returned by the verifiedOnly filter even though no CHIT validation happened.

Also applies to: 204-205

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

In `@pmoves/ui/app/api/graphiti/trails/route.ts` around lines 67 - 84,
toTrailEntry currently treats any presence of raw.sig as verified and sets
signatureValid to true; replace that with an actual CHIT signature validation
call and use its boolean result. In toTrailEntry, call the project’s CHIT
verification helper (e.g., verifyChitSignature or validateChit) with raw.sig
(and required context like raw.agent_id/raw.timestamp), set signatureValid to
the returned boolean, and set isVerified = !!raw.sig && signatureValid. Also
update the verifiedOnly filter (the code that checks for verified entries) to
filter on signatureValid rather than raw.sig presence so only cryptographically
validated entries are returned.
pmoves/ui/app/dashboard/jellyfin/page.tsx-24-40 (1)

24-40: ⚠️ Potential issue | 🟠 Major

Initial sync-status failures are silently dropped.

Both refreshSyncStatus and the mount-time load ignore result.ok === false, so a failed status fetch leaves error as null even though SyncStatus can render one. Reusing refreshSyncStatus() from the effect would also keep the failure handling in one place.

Suggested fix
   const refreshSyncStatus = useCallback(async () => {
     const result = await jellyfinSyncStatus();
     if (result.ok) {
       setSyncStatus(result.data);
+      setError(null);
+    } else {
+      setError(result.error);
     }
   }, []);
 
-  // Initial load - fetch sync status on mount
   useEffect(() => {
-    const loadInitialStatus = async () => {
-      const result = await jellyfinSyncStatus();
-      if (result.ok) {
-        setSyncStatus(result.data);
-      }
-    };
-    loadInitialStatus();
-  }, []);
+    void refreshSyncStatus();
+  }, [refreshSyncStatus]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/dashboard/jellyfin/page.tsx` around lines 24 - 40,
refreshSyncStatus currently ignores failed responses and the mount effect
duplicates the fetch; update refreshSyncStatus to handle when
jellyfinSyncStatus() returns result.ok === false by calling the component's
error state setter (e.g. setError or setSyncError) with the failure details and
optionally clearing setSyncStatus on error, keep the existing
setSyncStatus(result.data) on success, and then simplify the useEffect to call
refreshSyncStatus() instead of duplicating the fetch logic so all failure
handling is in one place.
pmoves/ui/app/api/graphiti/trails/route.ts-146-149 (1)

146-149: ⚠️ Potential issue | 🟠 Major

Clamp negative limit values before slicing.

parseInt('-1', 10) survives this check, and slice(0, -1) returns almost the whole array instead of zero/default results. The route should bound limit at the lower end too.

Suggested fix
   const limitParam = searchParams.get('limit');
   const limitParsed = parseInt(limitParam || '50', 10);
-  const limit = isNaN(limitParsed) ? 50 : Math.min(limitParsed, 200);
+  const limit = Number.isNaN(limitParsed)
+    ? 50
+    : Math.max(0, Math.min(limitParsed, 200));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/graphiti/trails/route.ts` around lines 146 - 149, The
current parsing of limitParam yields a negative limit (e.g., parseInt('-1'))
that passes the isNaN check and then becomes negative via Math.min, causing
slice(0, limit) to behave incorrectly; update the assignment for limit (using
the existing variables limitParam and limitParsed) to clamp values into the
valid range before slicing — treat NaN or missing as the default 50 and
otherwise bound limitParsed between 0 and 200 (e.g., use a max with 0 around the
existing min with 200) so negative values become 0 and cannot cause slice to
return unintended results.
pmoves/ui/e2e/ingestion.spec.ts-21-21 (1)

21-21: ⚠️ Potential issue | 🟠 Major

Line 21 is a no-op that should fail the test if the bulk-actions bar is not properly hidden in the initial state.

locator.isVisible() returns a Promise<boolean> and requires await. Without awaiting or asserting the result, this line does nothing and the test will pass regardless of whether the bar is visible. Given the test context (no items selected), the bar should not be visible initially.

Suggested fix
-    page.locator('[data-testid="bulk-actions-bar"]').isVisible();
-    // Might not be visible if no items selected
+    await expect(page.locator('[data-testid="bulk-actions-bar"]')).not.toBeVisible();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/ingestion.spec.ts` at line 21, The call to
page.locator('[data-testid="bulk-actions-bar"]').isVisible() is a no-op because
isVisible() returns a Promise<boolean>; change it to await the result and assert
it is not visible (either use Playwright's expect: await
expect(page.locator('[data-testid="bulk-actions-bar"]')).not.toBeVisible() or
await const visible = await
page.locator('[data-testid="bulk-actions-bar"]').isVisible();
expect(visible).toBe(false)) so the test fails when the bulk-actions bar is
improperly visible.
pmoves/ui/lib/resilience.ts-336-346 (1)

336-346: ⚠️ Potential issue | 🟠 Major

resilientFetch currently skips retries for HTTP 5xx responses.

Because fetch() resolves for HTTP errors, retry/circuit logic only triggers on thrown errors. As written, Line 336–346 treats 5xx as success, so no backoff happens.

Suggested fix
   return circuitBreaker.execute(() =>
     retry(
-      () =>
-        fetch(url, {
-          ...init,
-          // Add timeout via AbortSignal if not provided
-          signal: init?.signal || AbortSignal.timeout(30000),
-        }),
+      async () => {
+        const response = await fetch(url, {
+          ...init,
+          // Add timeout via AbortSignal if not provided
+          signal: init?.signal || AbortSignal.timeout(30000),
+        });
+        if (response.status >= 500) {
+          throw new Error(`HTTP ${response.status}`);
+        }
+        return response;
+      },
       options?.retry
     )
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/lib/resilience.ts` around lines 336 - 346, The current
resilientFetch call wraps fetch in retry/circuit but treats HTTP 5xx responses
as successful because fetch resolves for HTTP errors; update the function passed
into retry (the lambda inside circuitBreaker.execute/retry) to inspect the
resolved Response and if response.status is in the 5xx range (>=500 && <600)
throw an Error (or a custom Error containing the response/status) so that retry
and the circuit breaker see it as a failure and perform backoff; keep the
existing abort/signal handling and propagate the successful Response when status
<500.
pmoves/ui/lib/api/flute.ts-421-441 (1)

421-441: ⚠️ Potential issue | 🟠 Major

Health check always returns healthy: true, ignoring actual response.

Line 432 unconditionally sets healthy: true, overwriting whatever the service actually responded with. If the service returns { healthy: false, ... } to indicate degraded state, this code will still report the service as healthy.

🐛 Proposed fix to preserve response value
     const data = (await response.json()) as FluteHealth;
-    return ok({ ...data, healthy: true });
+    return ok(data);

If you intend to derive healthy from HTTP 200 status (treating any 2xx as healthy regardless of body), make this explicit:

     const data = (await response.json()) as Partial<Omit<FluteHealth, 'healthy'>>;
-    return ok({ ...data, healthy: true });
+    return ok({ healthy: true, ...data });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/lib/api/flute.ts` around lines 421 - 441, The fluteHealth function
currently overwrites the service's reported health by returning ok({ ...data,
healthy: true }); instead preserve the service's value: return the parsed data
as-is (e.g., return ok(data) or return ok({ ...data }) so data.healthy is not
forced to true). If your intent was to treat any 2xx as healthy instead, make
that explicit by deriving healthy from the response (e.g., set healthy =
data.healthy ?? true) before returning; update fluteHealth (and keep existing
error logging via logError and ErrorIds.FLUTE_HEALTH_CHECK_FAILED intact)
accordingly.
🟡 Minor comments (6)
pmoves/ui/e2e/services-health.spec.ts-232-240 (1)

232-240: ⚠️ Potential issue | 🟡 Minor

Test name doesn't match test behavior.

The test is named "shows error messages for failed health checks" but only verifies that the API returns JSON with a status property. It doesn't verify that error messages are actually displayed to the user.

Either rename the test to match its actual behavior or add assertions for error message display.

♻️ Option 1: Rename to match actual behavior
-  test('shows error messages for failed health checks', async ({ page }) => {
+  test('returns valid JSON even in degraded state', async ({ page }) => {
     const response = await page.request.get('/api/health');

     // Even in degraded state, should return proper JSON
     expect(response.headers()['content-type']).toContain('application/json');

     const body = await response.json();
     expect(body).toHaveProperty('status');
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/e2e/services-health.spec.ts` around lines 232 - 240, The test named
"shows error messages for failed health checks" only verifies the API JSON and
not any UI error display; either rename the test to reflect the current behavior
(e.g., "returns JSON status for /api/health") by updating the test title string
in the test(...) call, or extend the test to assert actual error messages are
displayed by navigating to the UI page (use page.goto or a UI route), locating
the error message element(s) and asserting visible text, or by asserting
specific error fields in the API response body (e.g., check body.errors or
body.message) before the UI checks; update the test title or add assertions
accordingly and keep the test name consistent with the assertions in the
test(...) block.
pmoves/ui/app/api/services/health-enhanced/route.ts-10-10 (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Reconsider edge runtime for health-enhanced route or ensure HEALTH_CHECK_HOST is consistently configured.

The runtime = 'edge' at line 10 combined with checkAllServices() at line 122 does probe service health endpoints, which use hardcoded localhost URLs from SERVICE_CATALOG. While the resolveHealthUrl() function in serviceHealth.ts mitigates this by rewriting localhost to HEALTH_CHECK_HOST (set to host.docker.internal in docker-compose), this is deployment-dependent. If HEALTH_CHECK_HOST is not configured in certain environments, health checks may fail silently or produce false negatives.

Additionally, the sibling route /api/health-all uses runtime = 'nodejs' for identical health checking logic, suggesting nodejs may be the safer choice here as well.

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

In `@pmoves/ui/app/api/services/health-enhanced/route.ts` at line 10, The route
currently sets export const runtime = 'edge' which, together with
checkAllServices() and SERVICE_CATALOG using localhost URLs, can cause health
checks to fail if HEALTH_CHECK_HOST is not configured; either change the runtime
to 'nodejs' (make the runtime string in this module match the sibling
/api/health-all) or ensure resolveHealthUrl() is always used and
HEALTH_CHECK_HOST is set in all deployments; update the file to export const
runtime = 'nodejs' OR add a deterministic fallback in resolveHealthUrl() so it
rewrites localhost to a safe host when HEALTH_CHECK_HOST is undefined,
referencing the symbols runtime, checkAllServices(), SERVICE_CATALOG,
resolveHealthUrl(), and HEALTH_CHECK_HOST.
pmoves/ui/app/dashboard/graphiti/page.tsx-17-18 (1)

17-18: ⚠️ Potential issue | 🟡 Minor

The agent dropdown narrows itself to the active filter.

After an agent is selected, the next response only contains that agent's entries, and uniqueAgents is rebuilt from that filtered payload. The dropdown then drops every other agent until the user resets back to “All”. Keep a separate unfiltered agent list or return filter metadata from the API.

Also applies to: 35-37, 87-93

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

In `@pmoves/ui/app/dashboard/graphiti/page.tsx` around lines 17 - 18, The dropdown
is being rebuilt from the filtered payload so selecting an agent removes other
agents; instead maintain an unfiltered agent list and do not overwrite it when
fetching filtered data: introduce or use a separate state (e.g., allAgents or
unfilteredAgents) and populate it from an unconditional fetch (or from API
metadata) while leaving the filtered response to populate only the displayed
rows; update places that currently rebuild uniqueAgents (the logic that runs
after fetch and any code that references
uniqueAgents/agentFilter/params.set("agentId", ...)) to preserve
unfilteredAgents for the dropdown and use filtered results only for the table.
pmoves/ui/lib/api/agent-zero.ts-142-157 (1)

142-157: ⚠️ Potential issue | 🟡 Minor

Manual timeout timers should be cleared in finally across all three request paths.

In these blocks, clearTimeout() runs only after successful fetch. Rejections leave timers pending.

Also applies to: 249-264, 351-363

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

In `@pmoves/ui/lib/api/agent-zero.ts` around lines 142 - 157, The timeoutId set
via setTimeout(() => controller.abort(), AGENT_ZERO_MCP_TIMEOUT) is only cleared
after a successful fetch, leaving timers pending on rejections; move the
clearTimeout(timeoutId) into a finally block that runs regardless of fetch
outcome for the fetch to `${getAgentZeroUrl()}/mcp/command` (and apply the same
change to the other two request paths in this file), ensuring you reference the
controller and timeoutId variables so the controller.signal remains usable and
the timer is always cleared.
pmoves/ui/lib/api/archon.ts-459-473 (1)

459-473: ⚠️ Potential issue | 🟡 Minor

Clear execution timeout in a finally block.

If fetch rejects before Line 472, the timer keeps running until 120s. Move clearTimeout(timeoutId) into finally.

Suggested fix
-    const response = await fetch(`${getArchonUrl()}/api/prompts/execute`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify(request),
-      signal: controller.signal,
-    });
-
-    clearTimeout(timeoutId);
+    const response = await fetch(`${getArchonUrl()}/api/prompts/execute`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(request),
+      signal: controller.signal,
+    });
-    const data = (await response.json()) as PromptExecutionResult;
-    return ok(data);
+    const data = (await response.json()) as PromptExecutionResult;
+    return ok(data);
   } catch (error) {
@@
-    return err(message);
+    return err(message);
+  } finally {
+    clearTimeout(timeoutId);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/lib/api/archon.ts` around lines 459 - 473, The timeout timer
created for the AbortController (const controller = new AbortController(); const
timeoutId = setTimeout(...)) is only cleared after a successful fetch, so if
fetch throws the timer remains active; wrap the fetch call in a try/finally
(e.g., try { const response = await
fetch(`${getArchonUrl()}/api/prompts/execute`, { ... , signal: controller.signal
}); } finally { clearTimeout(timeoutId); }) so clearTimeout(timeoutId) always
runs, keeping controller and signal usage the same and leaving response handling
unchanged.
pmoves/ui/lib/api/flute.ts-50-53 (1)

50-53: ⚠️ Potential issue | 🟡 Minor

Fragile port substitution in WebSocket URL derivation.

The hardcoded .replace(':8055', ':8056') only works if the HTTP URL contains exactly :8055. If the URL uses a different port or omits the port (using default 80/443), this replacement silently fails, leaving the WebSocket on the wrong port.

🔧 Proposed fix using URL parsing
   // Derive WS URL from HTTP URL
   const httpUrl = getFluteUrl();
-  return httpUrl.replace(/^http/, 'ws').replace(':8055', ':8056');
+  try {
+    const url = new URL(httpUrl);
+    url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
+    url.port = String(FLUTE_SERVICE_CONFIG.wsPort);
+    return url.toString().replace(/\/$/, '');
+  } catch {
+    // Fallback for non-standard URLs
+    return httpUrl.replace(/^http/, 'ws').replace(`:${FLUTE_SERVICE_CONFIG.defaultPort}`, `:${FLUTE_SERVICE_CONFIG.wsPort}`);
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/lib/api/flute.ts` around lines 50 - 53, The current WS derivation
in the code that calls getFluteUrl() is fragile because it does a
string.replace(':8055', ':8056'); instead parse the URL using the URL
constructor, switch protocol from http->ws or https->wss, and only change the
port when it is explicitly '8055' (leave other ports or absent ports alone so
default ports are preserved); then return url.toString(). Update the logic where
the WS URL is derived (the block that sets httpUrl = getFluteUrl() and does the
replace) to use URL parsing and the described port/protocol adjustments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d7ed12a5-e81d-445c-99ea-66db5b3d8e46

📥 Commits

Reviewing files that changed from the base of the PR and between e792296 and ea7c4fc.

📒 Files selected for processing (68)
  • .github/workflows/ui-tests.yml
  • pmoves/ui/__tests__/serviceHealth.test.ts
  • pmoves/ui/__tests__/utils/test-helpers.ts
  • pmoves/ui/app/api/agents/taxonomy/route.ts
  • pmoves/ui/app/api/chat/messages/route.ts
  • pmoves/ui/app/api/github/prs/route.ts
  • pmoves/ui/app/api/graphiti/trails/route.ts
  • pmoves/ui/app/api/health-all/route.ts
  • pmoves/ui/app/api/health/boot-jwt/route.ts
  • pmoves/ui/app/api/health/ingest-smoke/route.ts
  • pmoves/ui/app/api/health/presign/route.ts
  • pmoves/ui/app/api/health/route.ts
  • pmoves/ui/app/api/services-hub/route.ts
  • pmoves/ui/app/api/services/health-enhanced/route.ts
  • pmoves/ui/app/api/uploads/persist/route.ts
  • pmoves/ui/app/api/uploads/presign/route.ts
  • pmoves/ui/app/dashboard/agents/page.tsx
  • pmoves/ui/app/dashboard/github/page.tsx
  • pmoves/ui/app/dashboard/graphiti/page.tsx
  • pmoves/ui/app/dashboard/ingestion-queue/page.tsx
  • pmoves/ui/app/dashboard/jellyfin/page.tsx
  • pmoves/ui/app/dashboard/research/page.tsx
  • pmoves/ui/app/dashboard/search/page.tsx
  • pmoves/ui/app/dashboard/services/page.tsx
  • pmoves/ui/components/DashboardNavigation.tsx
  • pmoves/ui/components/hub/SystemStatsBar.tsx
  • pmoves/ui/components/ingestion/ApprovalRulesConfig.test.tsx
  • pmoves/ui/components/ingestion/ApprovalRulesConfig.tsx
  • pmoves/ui/components/ingestion/BulkApprovalActions.test.tsx
  • pmoves/ui/components/ingestion/BulkApprovalActions.tsx
  • pmoves/ui/components/jellyfin/BackfillControls.tsx
  • pmoves/ui/components/jellyfin/JellyfinMediaBrowser.tsx
  • pmoves/ui/components/jellyfin/SyncStatus.test.tsx
  • pmoves/ui/components/jellyfin/SyncStatus.tsx
  • pmoves/ui/components/research/ResearchResults.tsx
  • pmoves/ui/components/research/ResearchTaskList.test.tsx
  • pmoves/ui/components/search/SearchBar.test.tsx
  • pmoves/ui/components/search/SearchBar.tsx
  • pmoves/ui/components/search/SearchFilters.test.tsx
  • pmoves/ui/components/services/TierOverview.tsx
  • pmoves/ui/components/tensorzero/FunctionEditor.tsx
  • pmoves/ui/components/tensorzero/SmartDefaultsSelector.tsx
  • pmoves/ui/components/tensorzero/api.ts
  • pmoves/ui/components/tensorzero/hooks.ts
  • pmoves/ui/components/tensorzero/smart-defaults.ts
  • pmoves/ui/components/tokenism/GeometricView.tsx
  • pmoves/ui/components/tokenism/ResultsPanel.tsx
  • pmoves/ui/e2e/archon-prompts.spec.ts
  • pmoves/ui/e2e/chat.spec.ts
  • pmoves/ui/e2e/ingestion.spec.ts
  • pmoves/ui/e2e/jellyfin.spec.ts
  • pmoves/ui/e2e/research.spec.ts
  • pmoves/ui/e2e/search.spec.ts
  • pmoves/ui/e2e/services-health.spec.ts
  • pmoves/ui/lib/api/agent-zero.ts
  • pmoves/ui/lib/api/archon.test.ts
  • pmoves/ui/lib/api/archon.ts
  • pmoves/ui/lib/api/flute.test.ts
  • pmoves/ui/lib/api/flute.ts
  • pmoves/ui/lib/api/hirag.test.ts
  • pmoves/ui/lib/api/index.ts
  • pmoves/ui/lib/constants/errorIds.ts
  • pmoves/ui/lib/fluteClient.ts
  • pmoves/ui/lib/resilience.ts
  • pmoves/ui/lib/serviceDiscovery.ts
  • pmoves/ui/lib/types/agents.ts
  • pmoves/ui/lib/types/github.ts
  • pmoves/ui/lib/types/graphiti.ts
💤 Files with no reviewable changes (6)
  • pmoves/ui/components/research/ResearchResults.tsx
  • pmoves/ui/components/hub/SystemStatsBar.tsx
  • pmoves/ui/components/jellyfin/BackfillControls.tsx
  • pmoves/ui/components/tensorzero/hooks.ts
  • pmoves/ui/components/tensorzero/SmartDefaultsSelector.tsx
  • pmoves/ui/app/dashboard/ingestion-queue/page.tsx
🚧 Files skipped from review as they are similar to previous changes (27)
  • pmoves/ui/components/tokenism/ResultsPanel.tsx
  • pmoves/ui/components/research/ResearchTaskList.test.tsx
  • pmoves/ui/e2e/search.spec.ts
  • pmoves/ui/app/api/services-hub/route.ts
  • pmoves/ui/app/api/health/route.ts
  • pmoves/ui/components/ingestion/ApprovalRulesConfig.test.tsx
  • pmoves/ui/tests/serviceHealth.test.ts
  • pmoves/ui/app/api/chat/messages/route.ts
  • pmoves/ui/components/ingestion/ApprovalRulesConfig.tsx
  • .github/workflows/ui-tests.yml
  • pmoves/ui/components/services/TierOverview.tsx
  • pmoves/ui/components/ingestion/BulkApprovalActions.test.tsx
  • pmoves/ui/components/search/SearchBar.tsx
  • pmoves/ui/app/api/uploads/presign/route.ts
  • pmoves/ui/lib/types/graphiti.ts
  • pmoves/ui/app/dashboard/search/page.tsx
  • pmoves/ui/components/jellyfin/SyncStatus.tsx
  • pmoves/ui/app/dashboard/github/page.tsx
  • pmoves/ui/components/jellyfin/JellyfinMediaBrowser.tsx
  • pmoves/ui/components/search/SearchBar.test.tsx
  • pmoves/ui/components/tensorzero/smart-defaults.ts
  • pmoves/ui/components/tokenism/GeometricView.tsx
  • pmoves/ui/app/api/health/ingest-smoke/route.ts
  • pmoves/ui/components/DashboardNavigation.tsx
  • pmoves/ui/app/api/health/presign/route.ts
  • pmoves/ui/lib/api/flute.test.ts
  • pmoves/ui/app/api/health/boot-jwt/route.ts

POWERFULMOVES added a commit that referenced this pull request Mar 14, 2026
Remove all API route, UI component, and E2E test changes that overlap
with PR #922 (feat/pmovesui-api-routes). This PR now contains only:

- GEOMETRY BUS integration documentation (8 Mermaid diagrams, NATS
  subject taxonomy, service guides, pipeline docs, CGP schema refs)
- CONCH integration map
- UI .gitignore: add test-results/ and playwright-report/

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hunnibear and others added 16 commits March 14, 2026 17:33
Edge runtime doesn't support localhost DNS resolution needed for
Docker network service health checks. Switch to nodejs runtime
to enable proper service connectivity checks.

Fixes health check failures when querying services on Docker network.
Standard base64 decode fails on JWT's base64url encoding.
Replace '-' → '+' and '_' → '/' before decoding to properly
extract JWT payload for validation.

Fixes JWT validation failures in health check endpoint.
Prevent quota exhaustion with in-memory rate limiter
(10 requests per minute per IP). Uses token bucket algorithm
with sliding window cleanup.

Protects presign service from abuse while enabling health checks.
Health checks should reflect actual dependency availability,
not just service status. Adds direct DB connection check
to Supabase postgres database.

- Add errorIds for consistent error tracking
- Extend API client with DB health method
- Return DB status in health response

Enables detection of DB connectivity issues separate from API layer.
Add comprehensive E2E test coverage:
- archon-prompts.spec.ts: Archon prompt form tests
- chat.spec.ts: Chat interface tests
- services-health.spec.ts: Service health integration tests
- ingest-smoke route: Health endpoint for ingestion pipeline

Provides automated validation of key user flows.
Add automated UI testing workflow that runs:
- Linting checks
- TypeScript type checking
- Unit tests
- E2E tests with Playwright

Provides CI gate for UI changes to prevent regressions.
- Fixed 7 ESLint errors:
  - research/page.tsx: Moved ref access to useEffect
  - ApprovalRulesConfig.tsx: Escaped quotes in JSX
  - SyncStatus.tsx: Removed unused lastRefreshed state
  - GeometricView.tsx: Added eslint-disable for valid setState pattern
  - SearchBar.tsx: Used lazy initialization for localStorage loading
  - jellyfin/page.tsx: Refactored to avoid setState-in-effect warning

- Fixed 57 TypeScript errors:
  - Added missing error ID constants (AGENT_ZERO_*, ARCHON_*, FLUTE_*)
  - Fixed test assertions: changed result.data.errors to result.error.errors
  - Fixed duplicate object keys (healthy specified more than once)
  - Fixed ServiceHealthStatus type narrowing
  - Added mergedAt property to GitHubPR interface
  - Added @ts-expect-error for jsonwebtoken dynamic import
  - Fixed CircuitBreaker options type with mergedOptions field
  - Fixed setTimeout mock return type in serviceHealth.test.ts

All ESLint errors resolved (0 errors, 66 pre-existing warnings)
All TypeScript errors resolved (0 errors)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes for PR 906 (fix/ui-health-check-runtime-and-auth):

TypeScript fixes:
- jellyfin/page.tsx: Fix triggerBackfill call to pass object {limit} instead of number
- jellyfin/page.tsx: Fix property names (Id→id, Name→name, Type→type, ProductionYear→productionYear)
- archon-prompts.spec.ts: Fix selectOption to use index instead of RegExp label

Test fixes:
- hirag.test.ts: Add clearHiragCache() to beforeEach to prevent cache pollution
- hirag.test.ts: Add logForDebugging to errorUtils mock
- flute.ts: Fix fluteEstimateDuration to return 0 for empty/whitespace input

Service discovery fixes:
- serviceDiscovery.ts: Remove import.meta references that fail in Jest
- serviceDiscovery.ts: Use safer env var access pattern for cross-platform support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix unused variable warnings by prefixing with _
- Remove unused imports and type-only imports where appropriate
- Fix unused expressions in E2E tests (count(), isVisible() results)
- Remove unused locator expressions and constants
- Add eslint-disable comment for legitimate setState in useEffect
- Fix TypeScript errors introduced by variable renames

Modified 35 files:
- 4 API route files
- 3 page component files
- 16 UI component files
- 10 test files (unit + E2E)
- 2 utility files

ESLint now passes with 0 warnings and 0 errors.
Fixes failing unit tests where err() and ok() were called but not
mocked in errorUtils mock factory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds new API endpoints for the pmovesui dashboard:
- Agent taxonomy endpoint with type/class/evolution filters
- GitHub PR status endpoint with draft state tracking
- Graphiti trails endpoint with CHIT verification
- Enhanced service health endpoint with tier statistics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds type definitions for:
- GitHub PR status with isDraft property
- Agent taxonomy with AgentType/AgentClass enums
- Graphiti trails with CHIT signature verification

These types support the new pmovesui API endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds dashboard pages that consume the new API endpoints:
- Agents dashboard with taxonomy visualization
- GitHub PR monitoring dashboard
- Graphiti trails viewer with CHIT signatures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 'agents', 'github', 'graphiti' to NavKey type
- Fix unused variable warnings by prefixing with _ or removing imports
- Remove unused statusFilter/sourceFilter props from BulkApprovalActions usage
- Clean up unused type imports in API route files
TypeScript Fixes:
- Add parseInt NaN validation in taxonomy route (tier, layerCount)
- Remove non-null assertion on optional secondaryType field
- Add parseInt NaN validation in github prs route (limit param)
- Add parseInt NaN validation in graphiti trails route (limit param)

Test Artifacts:
- Remove test-results/ from git tracking (test artifacts should be gitignored)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@POWERFULMOVES
POWERFULMOVES force-pushed the feat/pmovesui-api-routes branch from ea7c4fc to e106f3d Compare March 14, 2026 21:34
@POWERFULMOVES
POWERFULMOVES merged commit e9bdd33 into main Mar 14, 2026
9 of 10 checks passed
@POWERFULMOVES
POWERFULMOVES deleted the feat/pmovesui-api-routes branch March 15, 2026 18:19
POWERFULMOVES added a commit that referenced this pull request Mar 15, 2026
The /api/audit/summary response included `docsRoot` — an absolute server
filesystem path — in the JSON body. This leaks internal directory structure
to unauthenticated clients. Remove it from the response payload.

Addresses: Z890 gap analysis Issue #7 (PR #922)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Mar 15, 2026
* fix(security): remove hardcoded CHIT passphrase from consciousness-service

Remove `pmoves-chit-default` default from Dockerfile ENV and main.py fallback.
Docker-compose enforces runtime injection via ${CHIT_PROD_PASSPHRASE:?...},
but the Dockerfile default was a security smell if the image ran standalone.

Addresses: Z890 gap analysis Issue #3 (PR #905)

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

* fix(security): add dev-mode warning and restrict role in cast-tts auth

Downgrade dev bypass role from "admin" to "dev" to limit privilege escalation
in development mode. Add logger.warning() when auth is bypassed so operators
can detect misconfiguration in production logs.

Addresses: Z890 gap analysis Issue #4 (PR #926)

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

* fix(security): warn on unauthenticated NATS fallback in flute-gateway

_build_nats_url() silently fell back to unauthenticated nats:// when
NATS_URL and NATS_USER/NATS_PASSWORD were all unset. Add logger.warning()
so operators can detect missing NATS credentials in logs.

Addresses: Z890 gap analysis Issue #5 (PR #927)

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

* fix(security): remove docsRoot path leak from audit summary API

The /api/audit/summary response included `docsRoot` — an absolute server
filesystem path — in the JSON body. This leaks internal directory structure
to unauthenticated clients. Remove it from the response payload.

Addresses: Z890 gap analysis Issue #7 (PR #922)

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

3 participants