Skip to content

feat: Fly Code — Jira, SECDESK, standup, command tray & security hardening#1464

Closed
tryan-mfly wants to merge 2 commits intopingdotgg:mainfrom
mediafly:feature/control-center-v2
Closed

feat: Fly Code — Jira, SECDESK, standup, command tray & security hardening#1464
tryan-mfly wants to merge 2 commits intopingdotgg:mainfrom
mediafly:feature/control-center-v2

Conversation

@tryan-mfly
Copy link
Copy Markdown

@tryan-mfly tryan-mfly commented Mar 27, 2026

Hi — this PR was opened against the wrong repo by mistake. Could you please delete it?

tryan-mfly and others added 2 commits March 23, 2026 13:26
…ow features

Fork of t3code extended with a complete developer workstation platform:

**Contracts & Model (Wave 1-2):**
- Extended OrchestrationProject with 14 Jira metadata fields
- 3 new commands (jira-metadata.update, note.update, touch) + events
- Decider, projector, and schemas handle new event types
- 4 new SQLite migrations (projects metadata, prompt history, Jira cache, specs)
- Web types and store hydration updated

**External Services (Wave 3):**
- Jira service: REST API with ~/.netrc auth, ADF text extraction, ticket CRUD
- Gmail service: gogcli CLI wrapper with email categorization (ACTION/REVIEW/FYI/NOISE)
- Calendar service: gcalcli CLI wrapper with TSV agenda parsing
- Full WS API wiring for all services (contracts → server → web)

**UI Features (Wave 4):**
- Sidebar: Jira ticket badges, status indicators, priority markers
- Sidebar: Per-project notes (persisted via events) and scratchpad (localStorage)
- Sidebar: Prompt history with click-to-resend
- Command Tray: Customizable bottom bar with /update, /triage, /commit presets
- Copy Terminal Output: Hover overlay button on terminal panes
- PR Creation Modal: 3-step flow (review → create → Jira comment)
- Spec/Planning Editor: Per-project textarea with debounced auto-save

**Orchestrated Workflows (Wave 5):**
- Context seeding: Auto-generates CLAUDE.md from Jira ticket data
- /update skill: Aggregates Jira, Gmail, Calendar for daily status reports
- /triage skill: Scans all channels, produces prioritized action list

36 files (18 modified, 18 new), 846 lines added. All 521 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d tray, and security hardening

Features:
- Rebrand from T3 Code to Fly Code
- Jira ticket import with auto-generated CLAUDE.md, summary.md, and agent context
- Auto-transition imported tickets to "In Progress"
- Filter Jira dropdown to exclude tickets with active projects
- SECDESK (JSM) ticket creation with bi-directional Jira linking
- SECDESK link persistence and auto-detection from summary.md
- Sidebar shows clickable SECDESK and Jira links per project
- Active/Waiting project status with context menu toggle
- Interactive daily standup message generator with copy-to-clipboard
- Side-by-side ticket comparison modal
- Command tray with configurable chat/terminal target routing
- AWS SSO login button (sends directly to terminal)
- Workflow rules in CLAUDE.md (IaC, PRs only)
- Bitbucket API scaffolding and credentials setup
- Backfill migration for pre-existing project ticket keys
- Feature overview PDF with Playwright screenshots

Security & Stability (12 fixes):
- Auth token removed from terminal process environments
- Centralized credential handling (eliminated 3 inline .netrc readers)
- Thread deletion no longer crashes server (Effect.catchCause for interrupts)
- SQLite busy_timeout prevents write contention under concurrent load
- Jira import rolls back on partial failure with specific error messages
- Forked event streams log errors instead of dying silently
- Checkpoint revert failures now surface as warnings
- SECDESK follow-up failures show warning toasts
- WebSocket listener errors logged for diagnostics
- Documented DrainableWorker serial safety for provider options maps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 89a21957-511d-4d1f-805c-df0ec5a49cd9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Mar 27, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 5 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

title: body.title,
start: body.start,
});
}
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 switch cases make handlers unreachable

Medium Severity

The WS_METHODS.gmailSearch, WS_METHODS.gmailMarkRead, WS_METHODS.gmailCreateDraft, WS_METHODS.calendarAgenda, and WS_METHODS.calendarMeetingPrep cases appear twice in the same switch statement. The second set (lines 1091–1130) is unreachable dead code since the first set (lines 995–1032) always matches first. This looks like an accidental copy-paste duplication.

Additional Locations (1)
Fix in Cursor Fix in Web

import { GmailError } from "../Errors.ts";
import type { GmailMessage } from "@t3tools/contracts";

const ACCOUNT = "tryan@mediafly.com";
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.

Hardcoded personal email in service code

High Severity

A personal email address tryan@mediafly.com is hardcoded as the ACCOUNT constant in GmailService.ts and repeated in ServiceHealth.ts. This makes the Gmail integration only work for a single developer and exposes personal information in the codebase. It needs to be configurable (e.g., via environment variable or settings).

Additional Locations (1)
Fix in Cursor Fix in Web

yield* runGogcli(
`gmail drafts create --account ${ACCOUNT} --to "${to}" --subject "${subject}" --body "${body}"${replyArg}`,
);
return { id: crypto.randomUUID() };
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.

Shell command injection via unsanitized user input

High Severity

User-provided strings (query, threadId, to, subject, body) from WebSocket requests are interpolated directly into shell commands via execSync. Double-quoting does not prevent shell injection — backticks and $() constructs within double quotes still execute. For example, a query containing $(malicious_command) would be executed by the shell. The same issue exists in CalendarService.ts where dateArg is unquoted.

Fix in Cursor Fix in Web

yield* sql`
UPDATE projection_projects
SET ticket_key = SUBSTR(title, 1, INSTR(title, ':') - 1),
jira_url = 'https://mediafly.atlassian.net/browse/' || SUBSTR(title, 1, INSTR(title, ':') - 1)
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.

Hardcoded Jira URL in database migration

Medium Severity

The backfill migration hardcodes https://mediafly.atlassian.net/browse/ directly in a SQL UPDATE statement. Unlike JiraService.ts which reads JIRA_BASE_URL from an environment variable, this migration permanently writes organization-specific URLs into the database with no way to configure it for other environments.

Fix in Cursor Fix in Web

`machine\\s+${host.replace(/\./g, "\\.")}\\s+login\\s+(\\S+)\\s+password\\s+(\\S+)`,
);
const match = content.match(machineRegex);
if (match) return { login: match[1]!, password: match[2]! };
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.

Jira netrc parser fails on multi-line format

Medium Severity

The readNetrcCredentials function in JiraService.ts uses a single regex that expects machine, login, and password on consecutive whitespace-separated tokens. A standard .netrc file typically has these on separate lines. The Bitbucket version in BitbucketApi.ts correctly parses a line-by-line format. The Jira regex will fail to match most real .netrc files where fields are on separate lines with varying indentation.

Fix in Cursor Fix in Web

@tryan-mfly tryan-mfly closed this Mar 27, 2026
@tryan-mfly
Copy link
Copy Markdown
Author

whoops, claude sent this to the wrong repo

Comment on lines +7 to +12
function runGcalcli(args: string): Effect.Effect<string, CalendarError> {
return Effect.try({
try: () => execSync(`gcalcli ${args}`, { encoding: "utf-8", timeout: 30000 }),
catch: (e) => new CalendarError({ message: `gcalcli failed: ${e}` }),
});
}
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.

🟡 Medium Layers/CalendarService.ts:7

runGcalcli interpolates args directly into a shell command string passed to execSync, so malicious input like "; rm -rf /" executes arbitrary shell commands. Pass args as an array to avoid shell interpretation.

+function runGcalcli(args: string[]): Effect.Effect<string, CalendarError> {
+  return Effect.try({
+    try: () => execSync("gcalcli", args, { encoding: "utf-8", timeout: 30000 }),
    catch: (e) => new CalendarError({ message: `gcalcli failed: ${e}` }),
  });
}
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/calendar/Layers/CalendarService.ts around lines 7-12:

`runGcalcli` interpolates `args` directly into a shell command string passed to `execSync`, so malicious input like `"; rm -rf /"` executes arbitrary shell commands. Pass `args` as an array to avoid shell interpretation.

Evidence trail:
apps/server/src/calendar/Layers/CalendarService.ts lines 7-10: `runGcalcli` function definition showing `execSync(\`gcalcli ${args}\`, ...)` pattern. Line 44 shows `runGcalcli(\`agenda ${dateArg} --tsv\`)` where `dateArg` comes from the `date` function parameter.

Comment on lines +83 to +91
createDraft: ({ to, subject, body, replyToMessageId }) =>
Effect.gen(function* () {
const replyArg = replyToMessageId ? ` --reply-to-message-id "${replyToMessageId}"` : "";
yield* runGogcli(
`gmail drafts create --account ${ACCOUNT} --to "${to}" --subject "${subject}" --body "${body}"${replyArg}`,
);
return { id: crypto.randomUUID() };
}),
}),
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.

🟢 Low Layers/GmailService.ts:83

createDraft returns crypto.randomUUID() instead of the actual draft ID from gogcli. Callers receive a fake ID that doesn't correspond to any Gmail draft, so subsequent operations by ID will fail. Consider parsing the gogcli output to extract and return the real draft ID.

     createDraft: ({ to, subject, body, replyToMessageId }) =>
       Effect.gen(function* () {
         const replyArg = replyToMessageId ? ` --reply-to-message-id "${replyToMessageId}"` : "";
-        yield* runGogcli(
+        const output = yield* runGogcli(
           `gmail drafts create --account ${ACCOUNT} --to "${to}" --subject "${subject}" --body "${body}"${replyArg}`,
         );
-        return { id: crypto.randomUUID() };
+        const draft = JSON.parse(output);
+        return { id: draft.id ?? draft.draftId ?? "" };
       }),
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/gmail/Layers/GmailService.ts around lines 83-91:

`createDraft` returns `crypto.randomUUID()` instead of the actual draft ID from `gogcli`. Callers receive a fake ID that doesn't correspond to any Gmail draft, so subsequent operations by ID will fail. Consider parsing the `gogcli` output to extract and return the real draft ID.

Evidence trail:
apps/server/src/gmail/Layers/GmailService.ts lines 79-86 at REVIEWED_COMMIT - shows `createDraft` implementation that runs `gogcli gmail drafts create` but ignores its output and returns `{ id: crypto.randomUUID() }` instead of parsing the actual draft ID from the command output.

Comment on lines +17 to +25
useEffect(() => {
ensureNativeApi()
.spec.get({ projectId })
.then((spec) => {
if (spec) setContent(spec.content);
setLoaded(true);
})
.catch(() => setLoaded(true));
}, [projectId]);
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.

🟢 Low components/SpecEditor.tsx:17

When projectId changes, the load effect fetches new spec data but does not cancel or ignore in-flight requests from the previous projectId. If an old request completes after the new one, setContent overwrites the editor with stale data from the wrong project. Consider adding a cancellation mechanism (e.g., an aborted flag set by the cleanup function) to ignore stale responses.

  // Load spec on mount
  useEffect(() => {
+    let aborted = false;
    ensureNativeApi()
      .spec.get({ projectId })
      .then((spec) => {
-        if (spec) setContent(spec.content);
+        if (!aborted && spec) setContent(spec.content);
-        setLoaded(true);
+        if (!aborted) setLoaded(true);
       })
       .catch(() => setLoaded(true));
+    return () => { aborted = true; };
   }, [projectId]);
Also found in 1 other location(s)

apps/web/src/components/chat/AuthStatusIndicators.tsx:25

Race condition in loginProvider allows concurrent login attempts. The check if (!api || loggingIn) return; on line 25 reads loggingIn from the closure captured at render time, not the current value. Since setLoggingIn(provider) on line 26 schedules an async state update, rapid clicks before React re-renders will each see loggingIn as null and proceed to call api.provider.login() multiple times concurrently.

🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/SpecEditor.tsx around lines 17-25:

When `projectId` changes, the load effect fetches new spec data but does not cancel or ignore in-flight requests from the previous `projectId`. If an old request completes after the new one, `setContent` overwrites the editor with stale data from the wrong project. Consider adding a cancellation mechanism (e.g., an `aborted` flag set by the cleanup function) to ignore stale responses.

Evidence trail:
apps/web/src/components/SpecEditor.tsx lines 17-24 at REVIEWED_COMMIT - useEffect with projectId dependency makes async call via ensureNativeApi().spec.get({ projectId }), calls setContent in .then() callback, has no cleanup function returning an abort flag or using AbortController

Also found in 1 other location(s):
- apps/web/src/components/chat/AuthStatusIndicators.tsx:25 -- Race condition in `loginProvider` allows concurrent login attempts. The check `if (!api || loggingIn) return;` on line 25 reads `loggingIn` from the closure captured at render time, not the current value. Since `setLoggingIn(provider)` on line 26 schedules an async state update, rapid clicks before React re-renders will each see `loggingIn` as `null` and proceed to call `api.provider.login()` multiple times concurrently.

...(row.suggestedRepo != null ? { suggestedRepo: row.suggestedRepo } : {}),
...(row.note != null ? { note: row.note } : {}),
...(row.lastAccessedAt != null ? { lastAccessedAt: row.lastAccessedAt } : {}),
...(row.archivedAt !== undefined ? { archivedAt: row.archivedAt } : {}),
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.

🟢 Low Layers/ProjectionSnapshotQuery.ts:572

Line 572 uses !== undefined for archivedAt, which incorrectly spreads { archivedAt: null } into the output when the database value is SQL NULL. All other optional fields like lastAccessedAt use != null to exclude both null and undefined. Consider changing to != null for consistency.

Suggested change
...(row.archivedAt !== undefined ? { archivedAt: row.archivedAt } : {}),
...(row.archivedAt != null ? { archivedAt: row.archivedAt } : {}),
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts around line 572:

Line 572 uses `!== undefined` for `archivedAt`, which incorrectly spreads `{ archivedAt: null }` into the output when the database value is SQL NULL. All other optional fields like `lastAccessedAt` use `!= null` to exclude both `null` and `undefined`. Consider changing to `!= null` for consistency.

Evidence trail:
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts lines 559-572 at REVIEWED_COMMIT - Shows 13 optional fields using `!= null` pattern (lines 559-571) and `archivedAt` using `!== undefined` (line 572).

Comment on lines +24 to +29
const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
Stream.runFold(
stream,
() => "",
(acc, chunk) => acc + new TextDecoder().decode(chunk),
);
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.

Layers/ServiceHealth.ts:24

Creating a new TextDecoder for each chunk splits multi-byte UTF-8 characters that span chunk boundaries, causing corruption (e.g., "€" becomes replacement characters). The collectStreamAsString helper instantiates TextDecoder inside the fold step, so every chunk boundary risks data loss for non-ASCII output.

-const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
-  Stream.runFold(
-    stream,
-    () => "",
-    (acc, chunk) => acc + new TextDecoder().decode(chunk),
-  );
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/provider/Layers/ServiceHealth.ts around lines 24-29:

Creating a new `TextDecoder` for each chunk splits multi-byte UTF-8 characters that span chunk boundaries, causing corruption (e.g., "€" becomes replacement characters). The `collectStreamAsString` helper instantiates `TextDecoder` inside the fold step, so every chunk boundary risks data loss for non-ASCII output.

Evidence trail:
apps/server/src/provider/Layers/ServiceHealth.ts lines 24-28 at REVIEWED_COMMIT show `new TextDecoder().decode(chunk)` being called inside the Stream.runFold step function, creating a fresh decoder for each chunk. TextDecoder default behavior (stream: false) outputs replacement characters for incomplete multi-byte sequences at chunk boundaries. See MDN TextDecoder documentation on the `stream` option.

sections.push("## Inbox Summary");
sections.push(`- ${unreadEmails.length} unread emails (${actionEmails} action, ${noiseEmails} noise)`);
sections.push(`- ${tickets.length} Jira tickets`);
sections.push(`- ${realMeetings.length} meetings today, ${tomorrowMeetings.length} tomorrow`);
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.

🟢 Low skills/triageSkill.ts:222

meetingsToday excludes all-day events (filtered at line 83), but meetingsTomorrow includes them. This causes inconsistent counts: all-day events appear in tomorrow's stats (line 236) and markdown summary (line 222) but not today's. Apply the same !m.isAllDay filter to tomorrowMeetings for consistency.

-  sections.push(`- ${realMeetings.length} meetings today, ${tomorrowMeetings.length} tomorrow`);
+  const realMeetingsTomorrow = tomorrowMeetings.filter((m) => !m.isAllDay);
+  sections.push(`- ${realMeetings.length} meetings today, ${realMeetingsTomorrow.length} tomorrow`);
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/skills/triageSkill.ts around line 222:

`meetingsToday` excludes all-day events (filtered at line 83), but `meetingsTomorrow` includes them. This causes inconsistent counts: all-day events appear in tomorrow's stats (line 236) and markdown summary (line 222) but not today's. Apply the same `!m.isAllDay` filter to `tomorrowMeetings` for consistency.

Evidence trail:
apps/server/src/skills/triageSkill.ts lines 43-49 (REVIEWED_COMMIT): `tomorrowMeetings` fetched without filtering
apps/server/src/skills/triageSkill.ts line 83 (REVIEWED_COMMIT): `realMeetings = todayMeetings.filter((m) => !m.isAllDay)` filters out all-day events for today
apps/server/src/skills/triageSkill.ts line 222 (REVIEWED_COMMIT): Uses `realMeetings.length` for today but `tomorrowMeetings.length` for tomorrow
apps/server/src/skills/triageSkill.ts lines 234-235 (REVIEWED_COMMIT): Stats use `realMeetings.length` for meetingsToday and `tomorrowMeetings.length` for meetingsTomorrow

Comment on lines +44 to +58
useEffect(() => {
if (!open) return;
setStep("select");
setCopied(false);
setYesterdayNotes("");
setBlockerNotes("");
setStandupText("");
setEntries(
projects.map((p) => ({
project: p,
included: true,
note: "",
})),
);
}, [open, projects]);
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.

🟡 Medium components/StandupModal.tsx:44

The useEffect at lines 44-58 depends on projects, so when the modal is open and projects updates (e.g., from syncServerReadModel), all user input (yesterdayNotes, blockerNotes, entries notes) is reset while the user is typing. This causes active data loss during server updates. Consider depending only on open to reset state when the modal opens, and ignore projects changes after initial load.

-  // Reset state when modal opens
-  useEffect(() => {
-    if (!open) return;
-    setStep("select");
-    setCopied(false);
-    setYesterdayNotes("");
-    setBlockerNotes("");
-    setStandupText("");
-    setEntries(
-      projects.map((p) => ({
-        project: p,
-        included: true,
-        note: "",
-      })),
-    );
-  }, [open, projects]);
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/StandupModal.tsx around lines 44-58:

The `useEffect` at lines 44-58 depends on `projects`, so when the modal is open and `projects` updates (e.g., from `syncServerReadModel`), all user input (`yesterdayNotes`, `blockerNotes`, `entries` notes) is reset while the user is typing. This causes active data loss during server updates. Consider depending only on `open` to reset state when the modal opens, and ignore `projects` changes after initial load.

Evidence trail:
apps/web/src/components/StandupModal.tsx lines 44-58 (REVIEWED_COMMIT): useEffect depends on [open, projects], and when open is true, it resets yesterdayNotes, blockerNotes, standupText to empty strings and entries to a fresh array. Line 45: `if (!open) return;` - the reset only happens when modal is open. Lines 46-56 show all state being reset including user input fields.

Comment on lines +112 to +116
function parseBitbucketRemoteInfo(url: string | null): BitbucketRemoteInfo | null {
const trimmed = url?.trim() ?? "";
if (trimmed.length === 0) return null;

// SSH: git@bitbucket.org:workspace/repo.git
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.

🟢 Low Layers/GitManager.ts:112

parseBitbucketRemoteInfo returns null for ssh://git@bitbucket.org/workspace/repo.git URLs because the SSH regex only matches the git@host:path format (colon separator). This causes resolveBitbucketRemote to fail silently and breaks Bitbucket PR operations when repos use the ssh:// protocol prefix. Consider adding ssh:// pattern support to match the GitHub parser at line 148.

  const trimmed = url?.trim() ?? "";
  if (trimmed.length === 0) return null;

+  // SSH protocol: ssh://git@bitbucket.org/workspace/repo.git
+  const sshProtocolMatch =
+    /^ssh:\/\/git@bitbucket\.org\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i.exec(trimmed);
+  if (sshProtocolMatch?.[1] && sshProtocolMatch[2]) {
+    return { workspace: sshProtocolMatch[1], repoSlug: sshProtocolMatch[2] };
+  }
+
   // SSH: git@bitbucket.org:workspace/repo.git
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/git/Layers/GitManager.ts around lines 112-116:

`parseBitbucketRemoteInfo` returns `null` for `ssh://git@bitbucket.org/workspace/repo.git` URLs because the SSH regex only matches the `git@host:path` format (colon separator). This causes `resolveBitbucketRemote` to fail silently and breaks Bitbucket PR operations when repos use the `ssh://` protocol prefix. Consider adding `ssh://` pattern support to match the GitHub parser at line 148.

Evidence trail:
apps/server/src/git/Layers/GitManager.ts lines 113-131 (parseBitbucketRemoteInfo function) at REVIEWED_COMMIT - SSH regex at line 118-119 only matches `git@bitbucket.org:` format. Lines 148-152 (parseGitHubRepositoryNameWithOwnerFromRemoteUrl function) shows GitHub parser includes `ssh://git@github.com/` pattern support.

Comment on lines +262 to +263
variant={copied ? "default" : "outline"}
onClick={() => void handleCopy()}
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.

🟢 Low components/StandupModal.tsx:262

When navigator.clipboard.writeText fails (e.g., permissions denied), the error is swallowed by the void operator in onClick={() => void handleCopy()}. The promise rejection is unhandled, so setCopied(true) never executes and the user receives no feedback that the copy failed. Consider adding .catch() to show an error state or using try/catch with an error toast.

-                onClick={() => void handleCopy()}
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/StandupModal.tsx around lines 262-263:

When `navigator.clipboard.writeText` fails (e.g., permissions denied), the error is swallowed by the `void` operator in `onClick={() => void handleCopy()}`. The promise rejection is unhandled, so `setCopied(true)` never executes and the user receives no feedback that the copy failed. Consider adding `.catch()` to show an error state or using `try/catch` with an error toast.

Evidence trail:
apps/web/src/components/StandupModal.tsx lines 146-150 (handleCopy function definition with no try/catch), line 263 (onClick handler with `void handleCopy()` and no .catch())

Comment on lines +22 to +29
function buildTree(entries: ReadonlyArray<ProjectEntry>): TreeNode[] {
// First pass: determine which paths are excluded (hidden entries and all their descendants)
const excluded = new Set<string>();
for (const entry of entries) {
const name = entry.path.split("/").pop() ?? entry.path;
if (isHiddenEntry(name)) excluded.add(entry.path);
if (entry.parentPath && excluded.has(entry.parentPath)) excluded.add(entry.path);
}
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.

🟢 Low components/FileBrowser.tsx:22

The first pass only excludes children of hidden directories when entries is sorted parent-first. If a child entry (e.g., .git/config) appears before its hidden parent (.git), the check at line 28 fails because the parent hasn't been added to excluded yet. This causes hidden directory contents to leak into the tree when the backend returns entries in arbitrary order.

  const excluded = new Set<string>();
-  for (const entry of entries) {
-    const name = entry.path.split("/").pop() ?? entry.path;
-    if (isHiddenEntry(name)) excluded.add(entry.path);
-    if (entry.parentPath && excluded.has(entry.parentPath)) excluded.add(entry.path);
-  }
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/FileBrowser.tsx around lines 22-29:

The first pass only excludes children of hidden directories when `entries` is sorted parent-first. If a child entry (e.g., `.git/config`) appears before its hidden parent (`.git`), the check at line 28 fails because the parent hasn't been added to `excluded` yet. This causes hidden directory contents to leak into the tree when the backend returns entries in arbitrary order.

Evidence trail:
- apps/web/src/components/FileBrowser.tsx lines 22-29: buildTree function with first-pass filtering logic
- apps/server/src/workspaceEntries.ts lines 144-151: compareRankedWorkspaceEntries shows entries sorted by score then path
- apps/server/src/workspaceEntries.ts lines 542-565: searchWorkspaceEntries returns rankedEntries sorted by score
- packages/contracts/src/project.ts lines 16-21: ProjectEntry schema showing parentPath field

@tryan-mfly
Copy link
Copy Markdown
Author

Hi — this PR was opened against the wrong repo by mistake. Could you please delete it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant