[AIG-637] Issue-to-PR GitHub/GitLab integration (M1-9)#26
Conversation
…bhook + CLI ingest
End-to-end "issue assigned to bot -> spawn coordinator + coder + reviewer
agents -> open draft PR" workflow reachable from a CLI command
(`aistack ingest issue <url>`) or via webhook (POST /v1/github/webhook,
POST /v1/gitlab/webhook).
New modules under src/github/: parses GitHub + GitLab issue URLs, fetches
the issue via the provider REST API (raw fetch, zero new deps), drives the
existing ReviewLoopCoordinator on the generated plan, renders a PR
description with plan + review iterations + audit-trail link, and posts a
draft PR/MR. Lifecycle labels (aistack-claimed/in-progress/
blocked-needs-human/done) are written back to the source issue on each
phase transition.
New transport routes register on the AIG-636 WebhookServer. To unblock
this branch ahead of AIG-636 landing on main, a minimal stub
src/transport/webhook.ts ships here with an interface compatible with
AIG-636 (WebhookServer + verifyHmacSignature). When AIG-636 merges first
the stub should be REPLACED (not deleted); both the GitHub and GitLab
route handlers import the stub via its public surface and need no changes.
GitHubConfig grows webhookSecret/gitlabWebhookSecret/gitlabToken/labels/
auditUrlTemplate fields (all optional, defaults preserved). CLI receives
a new `ingest` Commander chain registered in src/cli/index.ts. No
modifications to existing webhook/daemon/review-loop code.
Tests: tests/unit/github/{ingest,coordinator,webhook}.test.ts cover URL
parsing, provider routing, label transitions, PR body rendering,
HMAC verification, and a live WebhookServer integration test (ping +
401 invalid signature + 202 non-actionable event).
|
Warning Review limit reached
More reviews will be available in 2 minutes and 59 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
📝 WalkthroughWalkthroughThis PR adds a complete end-to-end GitHub/GitLab issue-to-draft-PR integration. It introduces provider clients for fetching issues, a workflow coordinator that orchestrates planning and review loops, webhook handlers for automated dispatch, and a CLI command for manual triggering. Issues progress through lifecycle labels (claimed → in-progress → blocked/done), and PR bodies include generated plans, adversarial review results, and audit trails. Dispatch deduplication prevents concurrent duplicate workflows. ChangesIssue-to-PR Workflow Integration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes The PR introduces significant new infrastructure across multiple layers (providers, orchestration, webhooks, CLI) with heterogeneous concerns: URL parsing, rate-limit handling, provider-specific API differences, label idempotency, HMAC signature verification, HTTP request handling, and workflow state management. While individual files are well-structured with clear responsibilities, reviewers must understand the cross-module data flow, the webhook dispatch lifecycle, lifecycle label semantics, and test mocking patterns. The change is substantial in scope (80 code ranges across 24 files) and introduces multiple new public contracts that affect downstream callers. Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
blackms
left a comment
There was a problem hiding this comment.
⚠️ Request changes — solid scaffold, but stub interface is NOT compatible with AIG-636 and idempotency is missing
Nice clean module structure: the provider abstraction (src/github/providers.ts), the lifecycle label state machine (src/github/labels.ts), and the PR-body renderer (src/github/pr-body.ts) are well-separated and tested. The HMAC implementation in the stub itself is correct (createHmac + timingSafeEqual with length guard, body buffered before verification so the digest covers the exact bytes signed). Auth handling is also fine: tokens are read from env into private fields, never logged, never echoed into error messages — only HTTP status / statusText / response text are surfaced.
That said, there are blocking issues before this can merge after AIG-636.
🚨 Blocking
1. The "forward-compatible stub" claim is false — interfaces diverge from AIG-636.
The PR description and the comment block at the top of src/transport/webhook.ts both promise that WebhookServer, verifyHmacSignature and WebhookHandler will remain backward-compatible with AIG-636's contract. They are not:
| Symbol | AIG-637 stub (this PR) | AIG-636 (#25) |
|---|---|---|
WebhookServer ctor |
new WebhookServer(options) |
new WebhookServer(runtime, config) — requires a DaemonRuntime |
| Route registration | addRoute({ path, handler, secret, signatureFormat, signatureHeader }) |
none — hard-coded POST /v1/tasks + GET /health |
verifyHmacSignature signature |
(payload, signature, secret, format) |
(body, secret, providedSignature) — arg order swapped, no format |
| Signature comparison | hex-string compare (sha256= prefixed) |
hex-decoded buffer compare |
WebhookHandler type |
exported here | does not exist in #25 |
Once AIG-636 lands on main, this stub cannot be "REPLACED" by it without rewriting github-webhook.ts and gitlab-webhook.ts, which both consume server.addRoute(...) and signatureFormat: 'github'|'gitlab'. Either AIG-636 needs to grow the routed surface this PR assumes (preferred — open an issue / coordinate with #25), or this PR needs to introduce an adapter layer (WebhookRouter) on top of AIG-636's DaemonRuntime-bound server.
2. No idempotency on issue → PR dispatch.
dispatchWorkflow in github-webhook.ts and gitlab-webhook.ts fires a brand-new workflow on every actionable event. Real-world consequences:
- GitHub frequently re-delivers webhooks (manual redelivery, network retry); two
issues.assignedevents for the same issue produce two coordinator runs, two plan-agent spawns, two review loops, and two draft PRs against two randomaistack/github-<n>-<uuid>branches. - The
aistack-claimedlabel is checked on the labeled path but isn't used as a guard against re-entry — a webhook can race with the label write done insiderunIssueToPRWorkflowstep 1. lastInvocationis a single mutable slot, so concurrent workflows for distinct issues clobber each other's promise reference (test-only helper, but the racing dispatches themselves are real).
Minimum mitigation: dedupe by (provider, owner, repo, issueNumber) with an in-flight set + completed cache (TTL on the order of hours), and short-circuit to 202 {dispatched:false,reason:'already in flight'} on collision. GitHub also sends X-GitHub-Delivery — log/store it for traceability.
3. No rate-limit / retry handling against GitHub or GitLab.
GitHubProvider.getIssue/setLabels/createPullRequest and the GitLab equivalents throw on any non-2xx, with zero awareness of 429, 403 rate limited, Retry-After, or X-RateLimit-Remaining: 0. On a busy bot user this will surface as GitHub setLabels failed: 403 Forbidden mid-workflow, leaving the issue in aistack-in-progress with no PR ever created. At minimum add:
- A
fetchhelper that inspects429/403+x-ratelimit-remaining: 0and waits theRetry-After/x-ratelimit-resetwindow once before re-throwing. - Differentiate auth failure (
401) from rate-limit so the coordinator can markblockedvs retry.
⚠️ Should fix before merge
4. coordinator.ts claim step happens after the URL parse but before any dedup, and the label is never set back to "claimed" on the labeled-event path — meaning the webhook flow that listens for aistack-claimed will trigger itself in a loop the first time it runs (it will re-apply the label, GitHub fires labeled again, etc.). Either suppress dispatch when the sender is the bot itself, or make label re-application not trigger.
**5. applyLifecycleLabel uses label.toLowerCase().startsWith('aistack-') to identify our labels — this will eat any user label starting with aistack- (e.g. aistack-discussion, aistack-roadmap). Restrict to the known set in DEFAULT_LABELS + overrides.
6. GitLab createPullRequest builds the body twice when draft: true:
body: JSON.stringify({
title: params.title, // <-- first title
...
...(params.draft ? { title: `Draft: ${params.title}` } : {}), // <-- overwrites
}),This works (object spread overrides) but is brittle and confusing — set const title = params.draft ? \Draft: ${params.title}` : params.title;` once and reuse.
**7. GitLab webhook signature mode treats X-Gitlab-Token as a shared-secret echo. This is correct for the legacy token mode, but GitLab 16+ supports an actual HMAC-signed X-Gitlab-Token when secret is enabled at a group level via Webhook secret token plus optional Verify SSL. Document the limitation in docs/GITHUB_INTEGRATION.md (currently silent on it).
8. webhook.ts stub defaults host to 0.0.0.0 — exposed to all interfaces with no TLS, no IP allowlist. AIG-636's webhook defaults to 127.0.0.1. Match that default to avoid accidentally publishing the unauthenticated bind in dev.
📝 Nits
dispatchWorkflowswallows errors with.catch((err) => log.error(...)); the rejection is logged but the underlyinglastInvocationpromise stays pending-then-fulfilled-with-undefined. Tests thatawait registered.lastInvocation()will seeundefinedeven on failure — fine for now but worth a comment.pr-body.tsuses_No plan produced_and similar italics-as-placeholder; for GitLab MR descriptions this renders fine, just confirm with the team that the#/!issue-ref split (issueRef = issue.provider === 'github' ? \#${issue.number}` : `!${issue.number}`) is intended (GitLab uses#for issues,!for MRs — this PR labels the issue as!` which is wrong).getIssuefor GitHub silently treats a webhook-styleissue(wherepull_requestfield is present) the same as a real issue; consider rejecting PRs masquerading as issues to avoid weird workflows onrepos/.../issues/<n>when n is actually a PR.randomUUID().slice(0, 8)for branch suffixes is fine, but consider using the issue'supdated_ator a hash of(provider, owner, repo, n)so a retried run resumes the same branch instead of creating an orphan.
🧪 Tests
The unit coverage on verifyHmacSignature, shouldDispatch, parseIssueUrl, applyLifecycleLabel, and the coordinator happy/sad paths is genuinely good. Missing: an idempotency test (fire two identical webhooks back-to-back, assert only one workflow runs), a 429/Retry-After test for the providers, and a self-hosted GHE base-URL test (the host.includes('api.github.com') check will misroute github.example.com).
Verdict: hold for changes. Items 1, 2, 3 are blockers; the rest are quick fixes. Happy to re-review once the stub is reconciled with #25's actual WebhookServer shape (or a coordination plan is documented).
… framework Resolves the merge conflict between AIG-637's local stub src/transport/webhook.ts and the real WebhookServer landed by AIG-636 on main. The two had incompatible public APIs (addRoute vs. fixed /v1/tasks; 4-arg vs 3-arg verifyHmacSignature). Resolution - src/transport/webhook.ts: taken from main verbatim (--theirs); now owns the daemon task-ingestion endpoint exclusively. - src/transport/integration-router.ts (new): the multi-route HTTP router previously hiding inside the stub, renamed to IntegrationRouter with verifyIntegrationSignature. Same multi-format HMAC (github sha256 / gitlab raw token) that the SCM handlers need. - src/transport/github-webhook.ts, src/transport/gitlab-webhook.ts: registerGitHubWebhook / registerGitLabWebhook now take an IntegrationRouter instead of WebhookServer. addRoute call site unchanged. - src/cli/commands/index.ts, src/cli/index.ts: trivial merge — kept both createIngestCommand and main's createDaemonCommand / createWatchCommand / createRunCommand. - tests/unit/github/webhook.test.ts: spins up IntegrationRouter instead of the (now incompatible) WebhookServer; imports verifyIntegrationSignature under the existing alias. - docs/GITHUB_INTEGRATION.md: doc-only — mention IntegrationRouter in the E2E fixture section. The GitHub/GitLab webhooks now listen on their own port (default 9091) alongside the daemon's task-ingestion webhook (default 8787). No behavior change to the issue->PR workflow, label transitions, or HMAC verification semantics.
…e-to-pr-github # Conflicts: # tests/unit/monitoring/health.test.ts
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/cli/commands/ingest.ts`:
- Line 23: When parsing the --max-iterations CLI option, validate that the value
is a positive integer before using it (don't rely on Number(...) blindly).
Replace the current conversion at the site where argv.max-iterations is read
(the code that calls Number(...) around line 57) with a parseInt-based parse and
then check Number.isInteger(value) && value > 0; if the check fails, print a
clear error and exit (or throw) so invalid inputs like "abc", "0", or "-1" are
rejected. Ensure the option declaration (--max-iterations) still has a sensible
default and propagate the validated integer (e.g., maxIterations) downstream.
- Around line 41-43: When calling ingestIssue, don't always pass
config.github.token; determine the provider from the URL first (use the existing
url value or helper that parses provider) and select the appropriate credentials
object (e.g., use config.github.token for GitHub URLs and config.gitlab.token
for GitLab URLs) before invoking ingestIssue; update the call site that
currently does ingestIssue(url, { credentials: { token: config.github.token } })
to compute the correct token based on provider and pass that token in the
credentials parameter.
In `@src/github/coordinator.ts`:
- Around line 216-219: The ReviewLoopCoordinator instance (loop) may not be
cleaned up if loop.start() throws; wrap the start/usage in a try/finally so
loop.cleanup() always runs: construct the ReviewLoopCoordinator
(ReviewLoopCoordinator, variable loop), then await loop.start() inside a try
block and call loop.cleanup() in the finally block, rethrow or return
appropriately so the original behavior (returning { reviews, approved } when
successful) is preserved while guaranteeing cleanup on errors.
- Around line 141-144: The dry-run branch (checking opts.dryRun) returns early
and skips the lifecycle completion that transitions issues out of
claimed/inProgress when skipLabels is false; move or modify the dry-run handling
so that before returning you invoke the same done-label/transition logic used in
the normal flow (the code that handles label transitions for issue and
skipLabels) — i.e., ensure the transition function that updates issue
labels/state (the same routine used later in this coordinator) is called for the
current issue when opts.dryRun is true, then log and return { status: 'success',
issue, branch, plan, reviews, prBody } as before.
- Around line 232-246: finishFailure currently hardcodes the lifecycle label
'blocked' when calling applyLifecycleLabel, which ignores any configured label
overrides; change the call in finishFailure to use the configured failure
lifecycle label from the issue/opts/config (e.g. read from IssueToPrOptions or
config.github.labels — locate where lifecycle labels are stored in your
codebase) and pass that value into applyLifecycleLabel instead of the literal
'blocked' so configured overrides are respected.
In `@src/github/providers.ts`:
- Around line 323-335: The two raw fetch calls inside the provider request flow
(the initial fetch and the retry) lack timeouts and can hang; wrap both calls
with an AbortController-based timeout: create an AbortController, set a timeout
(e.g., providerRequestTimeoutMs) that calls controller.abort(), attach
controller.signal to the fetch init (ensure you clone/merge the existing init
into a new object with signal), clear the timer after fetch resolves, and reuse
the same timeout logic for the retry fetch as well; update the code paths around
isRateLimited, retryDelayMs, and delay so both fetch(input, init) invocations
use the abort-capable init.
In `@src/transport/github-webhook.ts`:
- Around line 152-159: The current handler for the GitHub "issues.labeled" event
(involving GitHubPayload and the issues.labeled branch) triggers dispatch when
the issue contains "aistack-claimed" rather than when that label was the one
added by the event; change the logic to inspect the event payload's label
(payload.label.name or similar) and only call dispatch when the added label
equals "aistack-claimed" (keep existing checks for action === "labeled" and use
the same dispatch call/site), so unrelated label changes do not trigger a rerun.
In `@src/utils/config.ts`:
- Around line 51-56: The IssueLabelSetSchema currently allows empty strings
which can propagate to applyLifecycleLabel and cause attempts to write empty
labels; update IssueLabelSetSchema so each label field uses a non-empty string
validator (e.g., z.string().min(1) or z.string().nonempty()) combined with
.optional() to reject "" while still allowing undefined, then run/type-check to
ensure callers (like applyLifecycleLabel) handle missing labels instead of empty
values.
In `@tests/unit/github/coordinator.test.ts`:
- Around line 52-66: The fakeIssue helper currently returns a fixed IssueDetails
object and ignores any caller overrides; update the fakeIssue function signature
to accept an optional overrides parameter (e.g., overrides?:
Partial<IssueDetails>) and spread it into the returned object so callers like
fakeIssue({ provider: 'gitlab', ... }) actually override fields; ensure default
fields remain and typing uses Partial<IssueDetails> for the parameter to keep
TypeScript happy.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0e270d35-a6e5-4abb-9d00-661492fd356b
📒 Files selected for processing (22)
docs/GITHUB_INTEGRATION.mdsrc/cli/commands/index.tssrc/cli/commands/ingest.tssrc/cli/index.tssrc/github/coordinator.tssrc/github/dispatch-dedupe.tssrc/github/index.tssrc/github/ingest.tssrc/github/labels.tssrc/github/pr-body.tssrc/github/providers.tssrc/transport/github-webhook.tssrc/transport/gitlab-webhook.tssrc/transport/integration-router.tssrc/types.tssrc/utils/config.tstests/unit/consensus-service.test.tstests/unit/daemon/webhook.test.tstests/unit/github/coordinator.test.tstests/unit/github/ingest.test.tstests/unit/github/webhook.test.tstests/unit/monitoring/health.test.ts
| interface GitHubPayload { | ||
| action?: string; | ||
| assignee?: { login?: string }; | ||
| issue?: { | ||
| html_url?: string; | ||
| number?: number; | ||
| labels?: Array<{ name?: string } | string>; | ||
| }; |
There was a problem hiding this comment.
Narrow issues.labeled dispatch to the label actually added.
At Line [188], dispatch is based on whether the issue currently contains aistack-claimed, not whether that was the label added by this event. This can trigger unnecessary reruns on unrelated label changes.
💡 Proposed fix
interface GitHubPayload {
action?: string;
assignee?: { login?: string };
+ label?: { name?: string };
issue?: {
html_url?: string;
number?: number;
labels?: Array<{ name?: string } | string>;
};
@@
if (event === 'issues' && payload.action === 'labeled') {
- const labels = payload.issue?.labels ?? [];
- const claimed = labels.some((l) =>
- typeof l === 'string' ? l === 'aistack-claimed' : l.name === 'aistack-claimed'
- );
- if (claimed) return { dispatch: true, reason: 'aistack-claimed label added' };
+ if (payload.label?.name === 'aistack-claimed') {
+ return { dispatch: true, reason: 'aistack-claimed label added' };
+ }
}Also applies to: 188-193
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/transport/github-webhook.ts` around lines 152 - 159, The current handler
for the GitHub "issues.labeled" event (involving GitHubPayload and the
issues.labeled branch) triggers dispatch when the issue contains
"aistack-claimed" rather than when that label was the one added by the event;
change the logic to inspect the event payload's label (payload.label.name or
similar) and only call dispatch when the added label equals "aistack-claimed"
(keep existing checks for action === "labeled" and use the same dispatch
call/site), so unrelated label changes do not trigger a rerun.
| const IssueLabelSetSchema = z.object({ | ||
| inProgress: z.string().optional(), | ||
| blocked: z.string().optional(), | ||
| done: z.string().optional(), | ||
| claimed: z.string().optional(), | ||
| }); |
There was a problem hiding this comment.
Reject empty lifecycle label names in config.
At Line 51–56, empty strings are currently valid. That can propagate to applyLifecycleLabel and attempt to write an empty label value to GitHub/GitLab APIs.
💡 Proposed fix
-const IssueLabelSetSchema = z.object({
- inProgress: z.string().optional(),
- blocked: z.string().optional(),
- done: z.string().optional(),
- claimed: z.string().optional(),
-});
+const NonEmptyLabelSchema = z.string().trim().min(1);
+
+const IssueLabelSetSchema = z.object({
+ inProgress: NonEmptyLabelSchema.optional(),
+ blocked: NonEmptyLabelSchema.optional(),
+ done: NonEmptyLabelSchema.optional(),
+ claimed: NonEmptyLabelSchema.optional(),
+});📝 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.
| const IssueLabelSetSchema = z.object({ | |
| inProgress: z.string().optional(), | |
| blocked: z.string().optional(), | |
| done: z.string().optional(), | |
| claimed: z.string().optional(), | |
| }); | |
| const NonEmptyLabelSchema = z.string().trim().min(1); | |
| const IssueLabelSetSchema = z.object({ | |
| inProgress: NonEmptyLabelSchema.optional(), | |
| blocked: NonEmptyLabelSchema.optional(), | |
| done: NonEmptyLabelSchema.optional(), | |
| claimed: NonEmptyLabelSchema.optional(), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/config.ts` around lines 51 - 56, The IssueLabelSetSchema currently
allows empty strings which can propagate to applyLifecycleLabel and cause
attempts to write empty labels; update IssueLabelSetSchema so each label field
uses a non-empty string validator (e.g., z.string().min(1) or
z.string().nonempty()) combined with .optional() to reject "" while still
allowing undefined, then run/type-check to ensure callers (like
applyLifecycleLabel) handle missing labels instead of empty values.
| function fakeIssue(): IssueDetails { | ||
| return { | ||
| provider: 'github', | ||
| host: 'github.com', | ||
| owner: 'octocat', | ||
| repo: 'hello', | ||
| number: 42, | ||
| title: 'Add new feature', | ||
| body: 'Please add X', | ||
| labels: ['enhancement'], | ||
| assignees: [], | ||
| htmlUrl: 'https://github.com/octocat/hello/issues/42', | ||
| state: 'open', | ||
| }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify mismatch: definition takes 0 args while a call passes 1 arg.
rg -n "function fakeIssue\\(" tests/unit/github/coordinator.test.ts -A6 -B2
rg -n "fakeIssue\\(" tests/unit/github/coordinator.test.ts -A2 -B2Repository: blackms/aistack
Length of output: 1946
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/unit/github/coordinator.test.ts"
echo "---- fakeIssue definition (lines ~45-110) ----"
nl -ba "$FILE" | sed -n '40,110p'
echo "---- call site with overrides (lines ~210-270) ----"
nl -ba "$FILE" | sed -n '210,270p'Repository: blackms/aistack
Length of output: 167
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/unit/github/coordinator.test.ts"
echo "---- fakeIssue definition (approx lines 40-110) ----"
cat -n "$FILE" | sed -n '40,110p'
echo "---- call site with overrides (approx lines 210-270) ----"
cat -n "$FILE" | sed -n '210,270p'Repository: blackms/aistack
Length of output: 4658
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find test runner / TS transpilation config
rg -n "ts-jest|swc|babel|transform|isolatedModules|transpileOnly|jest\\.config|ts-node" package.json jest.config.* . 2>/dev/null || trueRepository: blackms/aistack
Length of output: 16027
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show relevant package.json scripts
cat -n package.json | sed -n '1,160p'
# Look for vitest/jest config
fd -t f "vitest.config.*|jest.config.*|tsconfig.*" -d 4 . || true
rg -n "vitest|vitest\\.config|jest|ts-jest|typecheck|isolatedModules|transpileOnly" package.json **/*vitest*.ts **/*vitest*.js **/*jest*.ts **/*jest*.js **/*tsconfig*.json 2>/dev/null || trueRepository: blackms/aistack
Length of output: 4279
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- tsconfig.json ----"
cat -n tsconfig.json | sed -n '1,220p'
echo "---- vitest.config.ts ----"
cat -n vitest.config.ts | sed -n '1,220p'
echo "---- vitest.integration.config.ts (if present) ----"
if [ -f vitest.integration.config.ts ]; then cat -n vitest.integration.config.ts | sed -n '1,220p'; fi
echo "---- vitest.e2e.config.ts (if present) ----"
if [ -f vitest.e2e.config.ts ]; then cat -n vitest.e2e.config.ts | sed -n '1,220p'; fiRepository: blackms/aistack
Length of output: 3357
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/unit/github/coordinator.test.ts"
rg -n "interface IssueDetails|type IssueDetails|IssueDetails" "$FILE" -SRepository: blackms/aistack
Length of output: 117
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/unit/github/coordinator.test.ts"
cat -n "$FILE" | sed -n '1,90p'Repository: blackms/aistack
Length of output: 3777
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for test-specific tsconfig or typecheck hooks
fd -t f "tsconfig.*" . -d 4 || true
rg -n "typecheck|tsc --noEmit|noEmitOnError|tsconfig\\.test|vitest.*tsconfig|esbuild.*tsconfig|tsconfig" . || trueRepository: blackms/aistack
Length of output: 1175
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "vite-plugin-checker|typecheck.*vitest|vitest.*typecheck|ts-jest|tsc --noEmit|esbuild.*ts|transpileOnly" . || trueRepository: blackms/aistack
Length of output: 110
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# show the type/definition area for IssueDetails
FILE="src/github/providers.js"
if [ -f "$FILE" ]; then
sed -n '1,220p' "$FILE" | cat -n
echo "---- search IssueDetails in file ----"
rg -n "IssueDetails" "$FILE" -n
else
echo "File not found: $FILE"
ls -la src/github
fiRepository: blackms/aistack
Length of output: 720
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_TS="src/github/providers.ts"
FILE_JS="src/github/providers.js"
ls -la src/github
echo "---- searching for IssueDetails ----"
rg -n "export (type|interface) IssueDetails|interface IssueDetails|type IssueDetails" src/github src -SRepository: blackms/aistack
Length of output: 1013
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/github/providers.ts"
# print around IssueDetails
sed -n '1,120p' "$FILE" | cat -n
echo "---- around export interface IssueDetails ----"
rg -n "export interface IssueDetails" "$FILE"
START=1
END=120
sed -n "${START},${END}p" "$FILE" | cat -nRepository: blackms/aistack
Length of output: 9333
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/github/pr-body.ts"
sed -n '1,240p' "$FILE" | cat -n
rg -n "htmlUrl|provider|issue|merge-request|!42|issue #" "$FILE"Repository: blackms/aistack
Length of output: 3135
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n eslint.config.js | sed -n '1,220p'Repository: blackms/aistack
Length of output: 876
Fix fakeIssue override handling to match its calls
tests/unit/github/coordinator.test.ts defines fakeIssue() with no parameters, but the GitLab-specific test calls fakeIssue({ provider: 'gitlab', ... }). Update fakeIssue to accept overrides and spread them so the overridden values are actually used.
💡 Proposed fix
-function fakeIssue(): IssueDetails {
+function fakeIssue(overrides: Partial<IssueDetails> = {}): IssueDetails {
return {
provider: 'github',
host: 'github.com',
owner: 'octocat',
repo: 'hello',
number: 42,
title: 'Add new feature',
body: 'Please add X',
labels: ['enhancement'],
assignees: [],
htmlUrl: 'https://github.com/octocat/hello/issues/42',
state: 'open',
+ ...overrides,
};
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/unit/github/coordinator.test.ts` around lines 52 - 66, The fakeIssue
helper currently returns a fixed IssueDetails object and ignores any caller
overrides; update the fakeIssue function signature to accept an optional
overrides parameter (e.g., overrides?: Partial<IssueDetails>) and spread it into
the returned object so callers like fakeIssue({ provider: 'gitlab', ... })
actually override fields; ensure default fields remain and typing uses
Partial<IssueDetails> for the parameter to keep TypeScript happy.
blackms
left a comment
There was a problem hiding this comment.
Reviewed current head after the issue-to-PR fixes and main merge. CI is green (build, lint, typecheck, tests) and CodeRabbit is green. The two remaining GitHub review-thread objects are stale against current code: github-webhook now dispatches from payload.label, and config now rejects empty lifecycle labels via a trimmed non-empty schema with test coverage.
Implements AIG-637.
Milestone: M1 - Production
Estimate: 8 pts
Summary
See commit message for full implementation details (schema, API, CLI, tests, docs).
Notes from PM agent
DEPENDS ON AIG-636 webhook framework. Branch ships a local stub at src/transport/webhook.ts that should resolve via 'git checkout --theirs' when AIG-636 lands first.
Auto-opened by aistack PM agent on 2026-05-28 10:22. Review with /review or human dispatch.
Summary by CodeRabbit
Release Notes
New Features
aistack ingest issue <url>CLI command for GitHub/GitLab issue ingestionDocumentation