Skip to content

Merge Dev into main to add gitlab functionality#97

Merged
factory-nizar merged 2 commits into
mainfrom
dev
Jun 4, 2026
Merged

Merge Dev into main to add gitlab functionality#97
factory-nizar merged 2 commits into
mainfrom
dev

Conversation

@factory-nizar

@factory-nizar factory-nizar commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Adding gitlab functionality

factory-nizar and others added 2 commits June 3, 2026 10:37
* feat(gitlab): add hello-world CI/CD template to validate include:remote

Adds a minimal gitlab/templates/review.yml job that prints a hello-world
message. Used to verify that a GitLab CI pipeline can pull the template
over HTTPS from raw.githubusercontent.com before we layer in the Docker
image and real review entrypoints.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): scaffold src/gitlab adapter (context, token, api, types)

Pure-addition first slice of GitLab support. Mirrors the shape of
src/github/ so subsequent entrypoints can swap adapter by PLATFORM env.

- src/gitlab/context.ts parses CI_* env vars into a normalized
  ParsedGitlabContext with MR + commit + user + inputs
- src/gitlab/token.ts reads GITLAB_TOKEN with OVERRIDE/CI_JOB_TOKEN
  fallbacks; throws a clear error when missing
- src/gitlab/api/client.ts is a thin fetch wrapper around GitLab v4
  exposing getMr/getMrChanges/listNotes/createNote/updateNote/
  createDiscussionOnDiff/updateMrDescription
- src/gitlab/types.ts defines GitlabMr, GitlabNote, GitlabDiscussion,
  GitlabPosition
- 16 new unit tests; full existing suite (381 -> 397) still passes

No GitHub code paths touched.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): add MCP server exposing MR operations to droid exec

src/mcp/gitlab-mr-server.ts mirrors src/mcp/github-pr-server.ts in shape
so the review prompt can stay platform-agnostic at the tool layer.

Tools registered:
- get_mr / get_mr_changes / list_mr_notes
- create_mr_note / update_mr_note (sticky tracking comment lifecycle)
- update_mr_description (for @droid fill)
- submit_review: posts an optional summary note plus N inline
  discussions anchored to diff positions; per-comment errors are
  collected so one bad position doesn't abort the batch.

Position objects are built from the MR's diff_refs (base/head/start SHA)
and switch new_line vs old_line based on RIGHT vs LEFT side, matching
the GitHub PR review tool's contract.

5 new unit tests cover summary, inline anchoring, LEFT-side mapping,
and partial-failure batching. Full suite: 402/402.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): add prepare + update-comment-link entrypoints and sticky-note logic

Implements the sticky tracking note lifecycle for GitLab MR pipelines:
prepare creates (or reuses) the running-state note before droid exec
runs; update-comment-link rewrites it with success/failure + pipeline
links after droid exec completes.

- src/gitlab/operations/tracking-note.ts builds the note body with a
  hidden <!-- droid-tracking-note --> marker so retries find and update
  the same note instead of creating duplicates. Renders pipeline/job
  links, security badge (when automatic_security_review is on), and an
  error-details <details> block on failure.

- src/entrypoints/gitlab-prepare.ts parses CI env, gates on
  merge_request_event + automatic_review, writes a JSON state file
  (DROID_STATE_FILE) for downstream steps.

- src/entrypoints/gitlab-update-comment-link.ts reads the state file
  and PUTs the final body. Skips cleanly when no review ran.

7 new tracking-note tests; full suite 402 -> 409. Also tightened two
gitlab-mr-server tests to satisfy noUncheckedIndexedAccess.

Cross-SCM include:remote was validated end-to-end against
gitlab.com pipeline #2568334623 (passed) before this commit.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): real CI/CD Component template (runtime-clone, no Docker)

Replaces the hello-world include-test template with a real droid-review
job. No Docker image dependency for v1; instead the job:

1. Uses the public oven/bun:1.2.11 image (Bun preinstalled)
2. apt-get installs git/curl/ca-certificates on top
3. Shallow-clones Factory-AI/droid-action at $DROID_ACTION_REF
4. bun install --frozen-lockfile
5. Runs src/entrypoints/gitlab-prepare.ts (creates sticky tracking note)
6. droid exec placeholder (real prompt + MCP wiring next commit)
7. after_script always runs src/entrypoints/gitlab-update-comment-link.ts
   so failures still rewrite the note to the failure state

Template uses GitLab's spec.inputs so it works equally well via
`include: component:` (post-Catalog publish) and `include: remote:`
(today). Inputs: automatic_review, review_depth, review_model,
reasoning_effort, droid_action_repo, droid_action_ref, stage.

Required CI variables on the consumer side: FACTORY_API_KEY, GITLAB_TOKEN.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): wire up real droid exec with MCP server + inline review

The template now installs the Droid CLI, registers the gitlab-mr MCP
server with the runtime CI env, and runs `droid exec` against a
review prompt that gitlab-prepare writes to /tmp/droid-prompts/droid-prompt.txt.

- src/gitlab/review-prompt.ts: minimal v1 review prompt builder
  embedding the MR diff and instructions to call submit_review with a
  single batched set of inline comments (max 8). Returns LGTM body
  when no issues found.
- src/entrypoints/gitlab-prepare.ts: after creating the sticky note,
  fetches MR + diff via the GitLab API and writes the prompt file;
  exposes promptPath on the state JSON.
- gitlab/templates/review.yml:
  * Installs Droid CLI via curl https://app.factory.ai/cli | sh
  * Skips droid exec if prepare set shouldRunReview=false
  * Registers MCP server: droid mcp add gitlab_mr <bun run server> --env ...
  * Runs: droid exec -f $DROID_PROMPT_FILE --output-format stream-json
    --skip-permissions-unsafe (plus optional --model/--reasoning-effort)
  * after_script reads /tmp/droid-error.txt for failure details
- 3 new prompt-builder tests; suite at 412 passing.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix(gitlab): wire review_depth presets via resolveReviewConfig

Previously, review_depth was a declared spec.input that was parsed and
threaded through gitlab-prepare but never consumed by the prompt
builder or the template -- so customers passing review_depth: shallow
silently still got Droid's default model.

This wires it end-to-end:
- gitlab-prepare now calls resolveReviewConfig({reviewModel,
  reasoningEffort, reviewDepth}) from src/utils/review-depth.ts (which
  already had the preset table: shallow -> kimi-k2-0711, deep -> gpt-5.2
  + high reasoning), reusing the same logic the GitHub flow uses.
- Explicit review_model / reasoning_effort still beat the depth preset
  (resolveReviewConfig handles priority).
- Resolved values are written to /tmp/droid-prompts/resolved-env.sh as
  shell exports (RESOLVED_MODEL / RESOLVED_REASONING_EFFORT), plus
  echoed into the state.json for visibility.
- Template sources the shim before constructing droid exec args and
  uses the RESOLVED_* values for --model / --reasoning-effort.

Adds 6 unit tests covering: default deep preset, shallow preset,
explicit model override, explicit effort override, both overrides
simultaneously, unknown depth fallback. Suite at 418 passing.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): two-pass review (candidates + validator) mirroring GitHub

Replaces the v1 single-pass bespoke prompt with the same two-pass flow
the GitHub Action uses: Pass 1 generates review_candidates.json without
posting; Pass 2 validates each candidate, writes review_validated.json,
and submits approved findings in a single batched call. The /review
skill (loaded by Droid at runtime) drives both passes; the prompts
here are the runtime harness that selects which pass + which tools.

Why two droid exec calls rather than one:
1. Tool gating — Pass 1 has NO access to gitlab_mr___submit_review so
   the model can't shortcut by posting raw candidates. --enabled-tools
   is fixed at process start, so a single exec can't switch mid-run.
2. Fresh-eyes context — Pass 2 re-reads the diff without memory of
   why each candidate was generated, dropping ~30-60% false positives.
3. Hard output checkpoint on disk between passes (resumable, debuggable).
4. Clean failure semantics — half-baked Pass 1 won't pollute Pass 2.

New files:
* src/gitlab/data/review-artifacts.ts — fetch + write mr.diff,
  existing_comments.json, mr_description.txt (mirror of GitHub's
  precomputed PR artifacts).
* src/gitlab/prompts/types.ts — shared prompt-context shape.
* src/gitlab/prompts/candidates.ts — Pass 1 prompt, ported from
  GitHub's review-candidates-prompt.ts with MR/project terminology.
* src/gitlab/prompts/validator.ts — Pass 2 prompt, ported from
  GitHub's review-validator-prompt.ts; uses gitlab_mr___submit_review
  and the new gitlab_mr___update_tracking_note tool.
* src/entrypoints/gitlab-prepare-validator.ts — overwrites the prompt
  file with Pass 2 between the two droid exec calls.
* test/gitlab/prompts.test.ts + test/gitlab/review-artifacts.test.ts
  — 14 new tests covering the two prompts and artifact computation.

MCP additions:
* src/mcp/gitlab-mr-server.ts — new update_tracking_note tool that
  reads DROID_MR_IID + DROID_TRACKING_NOTE_ID from env, symmetric
  with GitHub's github_comment___update_droid_comment.

Template changes (gitlab/templates/review.yml):
* Sources resolved-env.sh once up front (now includes DROID_MR_IID
  + DROID_TRACKING_NOTE_ID).
* Registers the MCP server with those env vars exposed.
* Pass 1 droid exec: --enabled-tools excludes submit_review.
* Runs gitlab-prepare-validator between the two execs.
* Pass 2 droid exec: --enabled-tools includes gitlab_mr___submit_review
  and gitlab_mr___update_tracking_note.
* Artifacts archive bumped to include review_candidates.json,
  review_validated.json, droid-prompt.txt for debugging.

Removed:
* src/gitlab/review-prompt.ts (v1 single-pass bespoke prompt).
* test/gitlab/review-prompt.test.ts.

Refactoring src/gitlab/prompts/ + src/create-prompt/ into a shared
src/core/review/ tree is queued as a follow-up PR.

All 432 tests pass; tsc + prettier clean.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): wire `settings` input pass-through (parity with GitHub)

Adds the same `settings` input the GitHub action exposes:
* spec.input on the CI/CD Component (JSON string OR path to JSON file).
* Plumbed via DROID_SETTINGS env var to gitlab-prepare.
* gitlab-prepare reuses base-action/src/setup-droid-settings.ts
  (zero GitHub deps, fully portable) to write ~/.factory/droid/settings.json
  before either droid exec call. Always sets enableAllProjectMcpServers=true
  to match the GitHub flow.
* context.inputs.settings surfaces the raw value.
* 2 new context tests; 434 tests pass.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): GH parity batch — custom droids, debug artifacts, telemetry, draft skip, stderr capture

- 1.6b Copy bundled .factory/droids from droid-action checkout into runner ~/.factory/droids before droid exec, mirroring the GitHub action's Setup Custom Droids step.
- 1.6c Stage debug artifacts (/tmp/droid-prompts/*, droid-error.txt) into .droid-debug/ inside CI_PROJECT_DIR in after_script so GitLab can upload them on failure (GitLab artifacts cannot reach outside CI_PROJECT_DIR).
- 1.6d Capture droid exec stream-json output to per-pass JSONL log files via tee + pipefail; parse session_id / numTurns / durationMs (+ legacy cost_usd) from the last completion event of each pass; render a <sub> summary line (44 turns • 11m 40s • $0.42) and a collapsible session-IDs block in the sticky tracking note.
- C7 Skip Draft: / WIP: MRs at the rules: level (GitLab-native equivalent of GitHub's draft == false workflow if:).
- C9 On droid exec failure, tail 60 lines of the per-pass log into /tmp/droid-error.txt so the existing error-details block in the sticky note surfaces real failure context instead of a generic message.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): expose include_suggestions input (A5)

Adds the include_suggestions spec input to the GitLab review template.
Threading was already in place: gitlab-prepare reads the INCLUDE_SUGGESTIONS env var, stores it in the on-disk state, gitlab-prepare-validator restores it, and both candidate + validator prompts already consume includeSuggestions to gate suggestion-block guidance. This commit just exposes the toggle as a customer-facing input. Mirrors the GitHub action's include_suggestions input.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): cache bun install cache across pipelines (C8)

Adds a cache: block keyed on droid_action_ref so bun's package cache survives across MR pipelines. BUN_INSTALL_CACHE_DIR is pinned to CI_PROJECT_DIR/.bun-cache because GitLab's cache mechanism can only persist paths inside the project directory. policy: pull-push lets every job benefit from prior downloads and contribute newly-fetched packages back to the cache. Mirrors the actions/cache pattern in the GitHub action.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): rate-limit + exponential backoff in GitlabClient (C1)

Wraps every GitlabClient.request call with retry-on-transient-failure logic, mirroring the implicit retry behaviour the GitHub action gets from Octokit. Retries are limited to a configurable maxRetries (default 5) for: 408, 429, 500, 502, 503, 504, and raw fetch network errors. On 429 the client honours the Retry-After header when present (seconds or HTTP-date), and otherwise uses exponential backoff with jitter clamped to maxDelayMs. Non-retryable 4xx errors (401, 403, 404, etc.) surface immediately as GitlabApiError.

The retry knobs are exposed via a third constructor argument so tests can use sub-millisecond delays without needing fake timers.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): paginate listNotes + add listDiscussions (C2)

Adds a private paginate<T>() helper that follows GitLab's standard pagination either via the X-Next-Page header (newer GitLab) or Link rel="next" (older GitLab + GitLab.com responses) with per_page=100 to minimise round-trips. listNotes now uses it and a new listDiscussions method is exposed for use cases that need to inspect existing diff discussions. Mirrors Octokit's implicit paginate-everything behaviour that the GitHub action gets for free.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Revert "feat(gitlab): paginate listNotes + add listDiscussions (C2)"

This reverts commit 7c721d1.

* Revert "feat(gitlab): rate-limit + exponential backoff in GitlabClient (C1)"

This reverts commit d70dbd7.

* Revert "feat(gitlab): cache bun install cache across pipelines (C8)"

This reverts commit 0d6c73b.

* revert(gitlab): drop draft-skip rules + stderr tail-60 capture

Removes two GitLab-only behaviours that don't exist in the GitHub action:

- Draft/WIP `rules: when: never` block. GitHub leaves draft handling to the user's workflow `if: ... draft == false` condition and doesn't bake it into the action. Customers who want to skip drafts can add the same condition to their own .gitlab-ci.yml.
- Stderr tail-60 capture into droid-error.txt. GitHub uses core.setFailed(error.message) which surfaces in the Actions run summary, not in the PR comment body — the comment just shows an actionFailed flag. Revert to the plain "droid exec pass N failed" message for parity.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): expose automatic_security_review input (A1, per-MR scope)

Wires the automatic_security_review toggle through the GitLab CI/CD Component. When set to "true", the security-reviewer subagent already bundled in .factory/droids/security-reviewer.md is spawned in parallel with the code-review subagents during Pass 1 (the conditional block in src/gitlab/prompts/candidates.ts already handles this via the securityReviewEnabled flag). Security findings are prefixed with [security] and flow through the same Pass-2 validator path as code-review findings, ultimately posted via a single batched submit_review call.

No plugin install is required: the security-review skill that the subagent invokes as its first action is built into the Droid CLI binary. Mirrors the GitHub action's automatic_security_review input for the per-MR flow only; the scheduled full-repo scan (which additionally uses threat-model-generation / commit-security-scan / vulnerability-validation skills from the security-engineer plugin) is a separate feature.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): expose security_block_on_critical/high inputs (A10)

Matches GitHub action's input surface for security_block_on_critical
(default "true") and security_block_on_high (default "false").
Like the GitHub action, the inputs are parsed into context but not
currently consumed by production code — they exist for surface-level
parity and forward compatibility when blocking logic lands.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* docs(gitlab): add gitlab-setup.md and README section for GitLab CI/CD Component

Documents the manual installation flow for the new GitLab CI/CD
Component (`gitlab/templates/review.yml`): prerequisites, full inputs
table, what the pipeline produces, how the two-pass review works,
what is intentionally not yet supported (comment triggers, fill mode,
scheduled scans), and troubleshooting steps.

README now has a short GitLab section that points to the full guide.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* docs(gitlab): add drop-in .gitlab-ci.yml samples under gitlab/examples/

Adds two consumer-facing sample files plus a README so users have a
canonical reference for what a Component-consuming .gitlab-ci.yml looks
like:

  * gitlab/examples/.gitlab-ci.minimal.yml — shortest possible, every
    default accepted, only the two required CI/CD variables wired.
  * gitlab/examples/.gitlab-ci.example.yml — annotated, every input
    spelled out with safe-default guidance, plus an optional
    @droid fill block (commented out) showing how to add fill.
  * gitlab/examples/README.md — table mapping each file to its use case
    and the variable-setup story.

Also updates docs/gitlab-setup.md:

  * Points readers at gitlab/examples/ instead of just inlining the
    snippet, so customers can clone the project, copy the file, done.
  * Drops the stale 'service account provisioning' reference now that
    the install-code-review skill no longer provisions SAs.
  * Tightens the GITLAB_TOKEN row to make the poster-identity story
    explicit: the token owner IS the poster, no API impersonation.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* docs(gitlab): hide internal inputs, trim setup.md, default ref to main

Customer-facing surface cleanup before GA:

  * gitlab/templates/review.yml: change droid_action_ref default from
    "dev" to "main" so customers who don't set it track stable.
    Description annotated 'Internal: most users leave at the default'.
  * docs/gitlab-setup.md: drop droid_action_ref, droid_action_repo,
    stage from the Inputs table — they're for self-hosted GitLab
    mirrors / advanced overrides, not standard customer config.
  * docs/gitlab-setup.md: drop everything from 'How it works' onward
    (How it works, What's not yet supported, Troubleshooting,
    Self-hosted GitLab). Those belong in deeper docs / runbooks; the
    setup page should be exactly setup.
  * docs/gitlab-setup.md: drop the 'pin to a release tag (e.g. v1)'
    advice — droid-action doesn't tag yet, and the @main URL pin is
    the canonical pattern.
  * gitlab/examples/.gitlab-ci.example.yml: drop droid_action_ref from
    the inputs block + drop the same line from the optional fill
    snippet. Update the comment to just explain the @main pin.
  * gitlab/examples/README.md: drop the 'custom stage' reference now
    that stage isn't documented as a user-facing knob.

The hidden inputs still exist in the template with sensible defaults,
so air-gapped customers mirroring droid-action can still override
droid_action_repo or pin droid_action_ref to a private SHA — they just
aren't part of the documented surface.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* docs(readme): drop GitLab non-support note from quick-start

Customer-facing surface should describe what is supported, not what
isn't. Caveats belong in deeper docs / runbooks if anywhere, not in the
README quick-start.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* docs(readme): tighten GitLab section wording

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* refactor(gitlab): rename template to droid-review.yml; ship as two-file pattern

Component template renamed from gitlab/templates/review.yml to
gitlab/templates/droid-review.yml. Customer drops a self-contained
factory/droid-review.yml in their project and adds a single
`include: - local:` line to their .gitlab-ci.yml, mirroring the
GitHub action's `.github/workflows/droid-review.yml` model.

Examples restructured to reflect the two-file layout. Docs + README
snippets updated accordingly.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* refactor(gitlab): move component to top-level templates/ for Catalog compatibility

GitLab CI/CD Catalog requires components to live at top-level templates/.
Move gitlab/templates/droid-review.yml to templates/droid-review.yml so
the repo is ready for Catalog publish (gitlab.com/factory-ai/droid-action)
without future renames. Add templates/README.md explaining the directory
ownership (mandated by Catalog, parallel to action.yml + .github/workflows/
for GitHub) and a headline comment in droid-review.yml. Update all
existing include URLs (README, docs, examples) to the new path.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(gitlab): point at gitlab.com/factory-components/droid-action mirror

Switch the customer-facing include form from a raw GitHub URL to GitLab's
native include: project: form pointing at the gitlab.com pull-mirror of
this repo. Customers no longer need github.com egress to fetch the
template — gitlab.com is sufficient.

Also flip the runtime droid_action_repo default from
github.com/Factory-AI/droid-action.git to
gitlab.com/factory-components/droid-action.git so the at-job clone of
the runtime source also goes through gitlab.com. Customers behind a
firewall that only permits gitlab.com + app.factory.ai can now run the
full flow.

Updated:
- README.md GitLab quick-start snippet (include: project: form)
- docs/gitlab-setup.md Step 2 snippet
- gitlab/examples/factory/droid-review.yml
- templates/README.md — documents include: project: (default), future
  include: component: form once Catalog-tagged, and a raw GitHub URL
  fallback for projects that can't reach gitlab.com
- templates/droid-review.yml headline comment + droid_action_repo default

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix(gitlab): address bot review nits on #93

- templates/droid-review.yml: remove top-level `stages:` block so the
  Component doesn't leak its stage list into the consuming project.
  Job-level `stage: $[[ inputs.stage ]]` is unchanged; consumers add
  the stage to their own `stages:` list (or omit it for the default).
- templates/droid-review.yml: tighten droid_action_ref docstring to
  "tag/branch" since the clone uses `git clone --branch` (SHAs would
  silently fail).
- src/gitlab/token.ts: drop CI_JOB_TOKEN fallback. It was always set
  in CI so the MissingGitlabTokenError never fired, producing
  confusing 401s on first API call instead of a clear "set GITLAB_TOKEN"
  message. CI_JOB_TOKEN also lacks scopes for notes/discussions.
- src/mcp/gitlab-mr-server.ts: require a line anchor on ReviewComment
  (line for RIGHT, old_line for LEFT) via .refine(); GitLab rejects
  unanchored diff discussions.
- src/mcp/gitlab-mr-server.ts: remove `.max(30)` cap on comments so
  large MRs don't fail the entire submit_review call.

Tests: 445/445 pass; tsc clean.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
#96)

* refactor(review): extract shared candidates+validator prompts to src/core/review/

Both Pass 1 (candidate generation) and Pass 2 (validator) prompts were
~95% identical between GitHub and GitLab — same skill invocation, same
output schema, same critical constraints. Differences were limited to
labels (PR/MR, Repo/Project), JSON meta keys (prNumber/mrIid,
baseRef/targetBranch), MCP tool names, and a handful of platform-specific
instruction lines.

Extract the shared structure into platform-agnostic builders under
src/core/review/prompts/ that take a ReviewTerminology shape, and convert
the four existing prompt files into thin adapters:

  src/core/review/prompts/
    types.ts        ReviewTerminology + ReviewPromptContext
    candidates.ts   generateCandidatesPrompt(ctx)
    validator.ts    generateValidatorPrompt(ctx)

  src/create-prompt/terminology.ts        GITHUB_TERMINOLOGY constant
  src/gitlab/prompts/terminology.ts       GITLAB_TERMINOLOGY + factory

Both platform-specific wrappers now just map their context shape onto
ReviewPromptContext and delegate. GitLab's submit_review needs the MR IID
re-asserted in the tool call, so we expose a gitlabTerminologyFor(mrIid)
helper that bakes it into the submit_review hint.

Net LOC is roughly even on this commit alone (+61), but ~280 LOC of
literal duplication is gone, and any future shared prompt (security
review/report when GitLab adds it) gets the shared scaffolding for free.

All 29 prompt-specific tests still pass on both platforms; full suite
445/445; tsc clean.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* refactor(review): share ReviewArtifactPaths type + GitLab uses central disk-write helper

The two platforms fetch review artifacts with fundamentally different
mechanics (GitHub shells out to git/gh CLI with shallow-clone unshallow
and a 50MB-buffered fallback; GitLab uses parallel REST calls), so the
fetchers stay platform-specific. But the on-disk shape is identical, so:

  src/core/review/artifacts/types.ts   ReviewArtifactPaths +
                                       ReviewArtifactContents shapes
  src/core/review/artifacts/write.ts   writeReviewArtifacts(outDir,
                                       contents, names) — mkdir + parallel
                                       writeFile×3

GitLab's computeReviewArtifacts now delegates the disk-write through the
shared helper. GitHub keeps its 3-helper public API (each does its own
write so tests pin those signatures); it just re-exports the shared type
via the existing ReviewArtifacts alias.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* refactor(review): hoist sticky-comment state/telemetry types + formatters to core

The two platforms render the sticky review-tracking comment with very
different mechanics today — GitHub builds it from inside the agent via
the github-comment-server MCP tool, while GitLab writes a rich state-
machine body from CI — so we can't share a single renderer. What we
*can* share is the contract:

  src/core/review/tracking/types.ts    ReviewTrackingState +
                                       ReviewTrackingTelemetry +
                                       ReviewTrackingFields
  src/core/review/tracking/format.ts   formatDurationMs(ms) +
                                       formatCostUsd(usd) — keeps the
                                       "1m 23s • $0.0042" rendering
                                       identical across platforms

GitLab's tracking-note.ts now imports those and re-exports the GitLab
aliases (TrackingNoteState, TrackingNoteTelemetry, TrackingNoteOptions)
so existing call sites stay untouched.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* refactor(review): extract @droid command parser to platform-agnostic core

The actual string-matching for `@droid fill`, `@droid review`,
`@droid security`, `@droid security --full`, and bare `@droid` is
identical between platforms. Move parseDroidCommand + DroidCommand +
ParsedCommand into:

  src/core/review/triggers/parse-command.ts

GitHub's command-parser keeps extractCommandFromContext (which scans
GitHub-payload-shaped events) and re-exports the shared types/parser
so existing imports from `../utils/command-parser` stay valid.

When the GitLab trigger path is ported (currently a pending task on
the GitLab side), it can reuse parseDroidCommand directly instead of
duplicating the regex set.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix(review): formatDurationMs rollover + single source of truth for GL security badge

Two pre-existing bugs surfaced by the bot review on PR #96 — both ship
on dev today, this just brings the fixes along with the refactor.

1. formatDurationMs returned "1m 60s" for ms values whose remainder
   seconds rounded up to 60 (e.g. ms=119600 → seconds=119.6 →
   minutes=1, remSec=round(59.6)=60). Round to whole seconds first, then
   split into minutes+seconds so the carry happens cleanly. Add
   regression coverage in test/core/review/tracking/format.test.ts
   covering the rollover boundary (119600/179600/239600 ms) plus the
   normal sub-second / sub-minute / multi-minute cases.

2. The GitLab security-badge URL in the prompt instruction
   (`security%20review-ran-blue`) didn't match the badge actually
   rendered by buildTrackingNoteBody (`security%20review-enabled-blue`,
   plus shields.io style params + logo). If the validator pass followed
   the prompt literally and the CI-prepended badge also fired, the MR
   could end up with two distinct security badges. Export the
   SECURITY_BADGE constant from tracking-note.ts and reference it from
   GITLAB_TERMINOLOGY.securityBadgeInstruction so the renderer is the
   single source of truth.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix(review): address local code-review findings on the refactor

Three small issues caught by a thorough re-review of the refactor diff:

1. `securityBadgeInstruction` wrapped the badge markdown in single
   backticks (```![security](…)```), so an LLM literally following the
   prompt would paste the badge inside an inline-code span and produce
   `![security](...)` rendered as code instead of an image. Rephrased
   to emit the raw markdown with an explicit "no surrounding backticks
   or code fences" caveat.

2. The security-subagent prompt block interpolated
   `${t.baseRefLabel.toLowerCase()}` which produced awkward bare
   phrases ("pr base ref" / "mr target branch") instead of the
   original prompts' "base ref" / "target branch". Added a dedicated
   `baseRefShortLabel` field on ReviewTerminology and use it in the
   narrative prose; the noun-prefixed `baseRefLabel` is still used in
   the structured <context> block where it matches today's wording.

3. format.ts docstring claimed the helpers are "shared between the
   GitHub MCP comment server and the GitLab tracking-note builder" but
   the GH side doesn't consume them yet. Softened the wording to
   reflect actual usage today plus the unification intent.

Tests: 451/451 still pass; tsc clean.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.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.

1 participant