Skip to content

Commit 7e4fc66

Browse files
Add GitHub fallback for skill loading, rebase on dev
- load-skill.ts: try local plugin cache first, then fetch from GitHub - Re-wire skill loading after rebase (review-prompt.ts removed by PR #59) - Inject skill into candidates and validator prompt templates Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
1 parent b2494b9 commit 7e4fc66

File tree

10 files changed

+100
-87
lines changed

10 files changed

+100
-87
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ Additional checks for this codebase:
182182
```
183183

184184
These guidelines are automatically loaded and injected into all review prompts (code review, security review, and validation passes). No workflow changes needed.
185+
185186
## Security Skills
186187

187188
The security review uses specialized Factory skills installed from the public `Factory-AI/skills` repository:

src/create-prompt/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ export type PromptCreationOptions = {
304304
includeActionsTools?: boolean;
305305
reviewArtifacts?: ReviewArtifacts;
306306
outputFilePath?: string;
307+
reviewSkillContent?: string;
307308
};
308309

309310
export async function createPrompt({
@@ -318,6 +319,7 @@ export async function createPrompt({
318319
includeActionsTools = false,
319320
reviewArtifacts,
320321
outputFilePath,
322+
reviewSkillContent,
321323
}: PromptCreationOptions) {
322324
try {
323325
const droidCommentId = commentId?.toString();
@@ -334,6 +336,10 @@ export async function createPrompt({
334336
preparedContext.outputFilePath = outputFilePath;
335337
}
336338

339+
if (reviewSkillContent) {
340+
preparedContext.reviewSkillContent = reviewSkillContent;
341+
}
342+
337343
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/droid-prompts`, {
338344
recursive: true,
339345
});

src/create-prompt/templates/review-candidates-prompt.ts

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { formatSkillSection } from "../../utils/load-skill";
12
import type { PreparedContext } from "../types";
23

34
export function generateReviewCandidatesPrompt(
@@ -30,7 +31,7 @@ export function generateReviewCandidatesPrompt(
3031
return `You are a senior staff software engineer and expert code reviewer.
3132
3233
Your task: Review PR #${prNumber} in ${repoFullName} and generate a JSON file with **high-confidence, actionable** review comments that pinpoint genuine issues.
33-
34+
${formatSkillSection(context.reviewSkillContent)}
3435
<context>
3536
Repo: ${repoFullName}
3637
PR Number: ${prNumber}
@@ -54,20 +55,7 @@ Precomputed data files:
5455
<review_guidelines>
5556
- You are currently checked out to the PR branch.
5657
- Review ALL modified files in the PR branch.
57-
- Focus on: functional correctness, syntax errors, logic bugs, broken dependencies/contracts/tests, security issues, and performance problems.
58-
- High-signal bug patterns to actively check for (only comment when evidenced in the diff):
59-
- Null/undefined/Optional dereferences; missing-key errors on untrusted/external dict/JSON payloads
60-
- Resource leaks (unclosed files/streams/connections; missing cleanup on error paths)
61-
- Injection vulnerabilities (SQL injection, XSS, command/template injection) and auth/security invariant violations
62-
- OAuth/CSRF invariants: state must be per-flow unpredictable and validated; avoid deterministic/predictable state or missing state checks
63-
- Concurrency/race/atomicity hazards (TOCTOU, lost updates, unsafe shared state, process/thread lifecycle bugs)
64-
- Missing error handling for critical operations (network, persistence, auth, migrations, external APIs)
65-
- Wrong-variable/shadowing mistakes; contract mismatches (serializer/validated_data, interfaces/abstract methods)
66-
- Type-assumption bugs (e.g., numeric ops on datetime/strings, ordering key type mismatches)
67-
- Offset/cursor/pagination semantic mismatches (off-by-one, prev/next behavior, commit semantics)
6858
- Do NOT duplicate comments already in \`${commentsPath}\`.
69-
- Only flag issues you are confident about—avoid speculative or stylistic nitpicks.
70-
- **Confidence calibration:** For each finding, honestly assess how certain you are. Mark findings as P0 only if you are virtually certain of a crash/exploit. Mark as P1 for high-confidence correctness/security issues. Use P2 for findings where the bug is plausible but you cannot fully verify the trigger path from the available context. This severity rating will be used downstream for filtering.
7159
</review_guidelines>
7260
7361
<triage_phase>
@@ -194,21 +182,9 @@ Write output to \`${reviewCandidatesPath}\` using this exact schema:
194182
- **comments**: Array of comment objects
195183
- \`path\`: Relative file path (e.g., "src/index.ts")
196184
- \`body\`: Comment text starting with priority tag [P0|P1|P2], then title, then 1 paragraph explanation
197-
If you have **high confidence** a fix will address the issue and won’t break CI, append a GitHub suggestion block:
198-
199-
\`\`\`suggestion
200-
<replacement code>
201-
\`\`\`
202-
203-
**Suggestion rules:**
204-
- Keep suggestion blocks ≤ 100 lines
205-
- Preserve exact leading whitespace
206-
- Use RIGHT-side anchors only; do not include removed/LEFT-side lines
207-
- For insert-only suggestions, repeat the anchor line unchanged, then append new lines
208185
- \`line\`: Target line number (single-line) or end line number (multi-line). Must be ≥ 0.
209186
- \`startLine\`: \`null\` for single-line comments, or start line number for multi-line comments
210-
- \`side\`: "RIGHT" for new/modified code (default). Use "LEFT" only for removed code **without** suggestions.
211-
If you include a suggestion block, choose a RIGHT-side anchor and keep it unchanged so the validator can reuse it.
187+
- \`side\`: "RIGHT" for new/modified code (default), "LEFT" only for removed code
212188
- \`commit_id\`: "${prHeadSha}"
213189
214190
- **reviewSummary**:

src/create-prompt/templates/review-validator-prompt.ts

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { formatSkillSection } from "../../utils/load-skill";
12
import type { PreparedContext } from "../types";
23

34
export function generateReviewValidatorPrompt(
@@ -33,7 +34,7 @@ export function generateReviewValidatorPrompt(
3334
return `You are validating candidate review comments for PR #${prNumber} in ${repoFullName}.
3435
3536
IMPORTANT: This is Phase 2 (validator) of a two-pass review pipeline.
36-
37+
${formatSkillSection(context.reviewSkillContent)}
3738
### Context
3839
3940
* Repo: ${repoFullName}
@@ -91,44 +92,11 @@ Read:
9192
9293
## Phase 2: Validate candidates
9394
94-
Apply the same Reporting Gate as review:
95-
96-
### Approve ONLY if at least one is true
97-
* Definite runtime failure
98-
* Incorrect logic with a concrete trigger path and wrong outcome
99-
* Security vulnerability with realistic exploit
100-
* Data corruption/loss
101-
* Breaking contract change (discoverable in code/tests)
95+
Apply the Reporting Gate, confidence calibration, and deduplication rules from the review methodology above.
10296
103-
Reject if ANY of these are true:
104-
* It's speculative / "might" without a concrete trigger
105-
* It's stylistic / naming / formatting
97+
Additionally reject if:
10698
* It's not anchored to a valid changed line
107-
* It's already reported (dedupe against existing comments)
108-
* The anchor (path/side/line/startLine) would need to change to make the suggestion work — reject instead
109-
* It flags missing error handling / try-catch for a code path that won't crash in practice (e.g., the caller already handles the error, or the input is validated upstream)
110-
* It describes a hypothetical race condition or timing issue without identifying the specific concurrent access pattern that triggers it
111-
* It's about code that appears in the diff but is not part of the PR's primary change — e.g., adjacent functions, unrelated files in a multi-subsystem PR, or code from a different PR's changes that happen to be visible in context
112-
113-
### Confidence-based filtering
114-
115-
Pay attention to the candidate's priority level:
116-
- **P0 findings**: Approve if the trigger path checks out. These should be definite crashes/exploits.
117-
- **P1 findings**: Approve if you can verify the logic error or security issue is real.
118-
- **P2 findings**: Reject by default. Only approve a P2 finding if ALL of these are true: (1) you can independently verify the bug exists by examining the code, (2) the bug has a concrete trigger that a user or caller could realistically hit, and (3) the finding is NOT about edge cases, defensive coding, or style. When in doubt about a P2, reject it.
119-
120-
### Deduplication (STRICT)
121-
122-
Before approving a candidate, check for duplicates:
123-
1. **Among candidates**: If two or more candidates describe the same underlying bug (same root cause, even if anchored to different lines or worded differently), approve only the ONE with the best anchor and clearest explanation. Reject the rest with reason "duplicate of candidate N".
124-
2. **Against existing comments**: If a candidate repeats an issue already covered by an existing PR comment (from \`${commentsPath}\`), reject it with reason "already reported in existing comments".
125-
3. Same file + overlapping line range + same issue = duplicate, even if the body text differs.
126-
127-
Suggestion block rules (minimal):
128-
* Preserve exact leading whitespace and keep blocks ≤ 100 lines
129-
* Use RIGHT-side anchors only; do not include removed/LEFT-side lines
130-
* For insert-only suggestions, repeat the anchor line unchanged, then append new lines
131-
* Do not change the anchor fields (path/side/line/startLine) from the candidate — only edit the body
99+
* It's already reported (dedupe against existing comments in \`${commentsPath}\`)
132100
133101
When rejecting, write a concise reason.
134102

src/create-prompt/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,5 @@ export type PreparedContext = CommonFields & {
118118
};
119119
reviewArtifacts?: ReviewArtifacts;
120120
outputFilePath?: string;
121+
reviewSkillContent?: string;
121122
};

src/entrypoints/generate-review-prompt.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { prepareMcpTools } from "../mcp/install-mcp-server";
1515
import { generateReviewCandidatesPrompt } from "../create-prompt/templates/review-candidates-prompt";
1616
import { generateSecurityReviewPrompt } from "../create-prompt/templates/security-review-prompt";
1717
import { normalizeDroidArgs, parseAllowedTools } from "../utils/parse-tools";
18+
import { loadSkill } from "../utils/load-skill";
1819

1920
async function run() {
2021
try {
@@ -97,6 +98,9 @@ async function run() {
9798
// to write structured findings for the combine step
9899
const outputFilePath = process.env.DROID_OUTPUT_FILE || undefined;
99100

101+
const reviewSkillContent =
102+
reviewType === "code" ? await loadSkill("review") : undefined;
103+
100104
await createPrompt({
101105
githubContext: context,
102106
commentId,
@@ -108,6 +112,7 @@ async function run() {
108112
generatePrompt,
109113
reviewArtifacts,
110114
outputFilePath,
115+
reviewSkillContent,
111116
});
112117

113118
// Set run type

src/mcp/github-pr-server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,12 @@ export function createGitHubPRServer({
624624
path: z
625625
.string()
626626
.describe("The file path to comment on (e.g., 'src/index.js')"),
627-
body: z.string().min(1).describe("The comment text (supports markdown and GitHub code suggestion blocks)"),
627+
body: z
628+
.string()
629+
.min(1)
630+
.describe(
631+
"The comment text (supports markdown and GitHub code suggestion blocks)",
632+
),
628633
line: z
629634
.number()
630635
.int()

src/tag/commands/review-validator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { prepareMcpTools } from "../../mcp/install-mcp-server";
99
import { normalizeDroidArgs, parseAllowedTools } from "../../utils/parse-tools";
1010
import type { PrepareResult } from "../../prepare/types";
1111
import { generateReviewValidatorPrompt } from "../../create-prompt/templates/review-validator-prompt";
12+
import { loadSkill } from "../../utils/load-skill";
1213

1314
export async function prepareReviewValidatorMode({
1415
context,
@@ -45,6 +46,8 @@ export async function prepareReviewValidatorMode({
4546
descriptionPath: `${promptsDir}/pr_description.txt`,
4647
};
4748

49+
const reviewSkillContent = await loadSkill("review");
50+
4851
await createPrompt({
4952
githubContext: context,
5053
commentId: trackingCommentId,
@@ -56,6 +59,7 @@ export async function prepareReviewValidatorMode({
5659
},
5760
generatePrompt: generateReviewValidatorPrompt,
5861
reviewArtifacts,
62+
reviewSkillContent,
5963
});
6064

6165
core.exportVariable("DROID_EXEC_RUN_TYPE", "droid-review");

src/tag/commands/review.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createInitialComment } from "../../github/operations/comments/create-in
99
import { normalizeDroidArgs, parseAllowedTools } from "../../utils/parse-tools";
1010
import { isEntityContext } from "../../github/context";
1111
import { generateReviewCandidatesPrompt } from "../../create-prompt/templates/review-candidates-prompt";
12+
import { loadSkill } from "../../utils/load-skill";
1213
import type { Octokits } from "../../github/api/client";
1314
import type { PrepareResult } from "../../prepare/types";
1415

@@ -84,6 +85,8 @@ export async function prepareReviewMode({
8485
githubToken,
8586
});
8687

88+
const reviewSkillContent = await loadSkill("review");
89+
8790
await createPrompt({
8891
githubContext: context,
8992
commentId,
@@ -95,6 +98,7 @@ export async function prepareReviewMode({
9598
},
9699
generatePrompt: generateReviewCandidatesPrompt,
97100
reviewArtifacts,
101+
reviewSkillContent,
98102
});
99103
core.exportVariable("DROID_EXEC_RUN_TYPE", "droid-review");
100104

src/utils/load-skill.ts

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { readFile, readdir } from "fs/promises";
22
import { resolve, join } from "path";
33

4+
const PLUGINS_REPO = "Factory-AI/factory-plugins";
5+
const PLUGINS_BRANCH = "main";
6+
47
const SHARED_BEGIN = "<!-- BEGIN_SHARED_METHODOLOGY -->";
58
const SHARED_END = "<!-- END_SHARED_METHODOLOGY -->";
69

710
/**
811
* Format skill content for inclusion in a CI prompt.
9-
* Extracts only the shared methodology (between markers) so deep-specific
10-
* instructions in the CI template are authoritative for execution behavior.
12+
* Extracts only the shared methodology (between markers) so CI-specific
13+
* instructions in the template remain authoritative for execution behavior.
1114
*/
1215
export function formatSkillSection(skillContent: string | undefined): string {
1316
if (!skillContent) return "";
@@ -34,38 +37,78 @@ export function extractSharedMethodology(content: string): string {
3437
}
3538

3639
/**
37-
* Load a skill from the core plugin cache.
40+
* Load a skill from the local core plugin cache.
3841
* The Droid CLI installs the core plugin to:
3942
* ~/.factory/plugins/cache/factory-plugins/core/<hash>/skills/<name>/SKILL.md
4043
*/
41-
export async function loadSkill(
44+
async function loadSkillFromCache(
4245
skillName: string,
4346
): Promise<string | undefined> {
4447
const home = process.env.HOME || "~";
4548
const cacheDir = resolve(home, ".factory/plugins/cache/factory-plugins/core");
4649

50+
let entries: string[];
4751
try {
48-
const entries = await readdir(cacheDir);
49-
for (const hash of entries) {
50-
const skillPath = join(cacheDir, hash, "skills", skillName, "SKILL.md");
51-
try {
52-
const content = await readFile(skillPath, "utf8");
53-
const trimmed = content.trim();
54-
if (!trimmed) continue;
55-
console.log(
56-
`Loaded skill ${skillName} from ${skillPath} (${trimmed.length} bytes)`,
57-
);
58-
return trimmed;
59-
} catch {
60-
continue;
61-
}
52+
entries = await readdir(cacheDir);
53+
} catch {
54+
return undefined;
55+
}
56+
57+
for (const hash of entries) {
58+
const skillPath = join(cacheDir, hash, "skills", skillName, "SKILL.md");
59+
try {
60+
const content = await readFile(skillPath, "utf8");
61+
const trimmed = content.trim();
62+
if (!trimmed) continue;
63+
console.log(
64+
`Loaded skill ${skillName} from ${skillPath} (${trimmed.length} bytes)`,
65+
);
66+
return trimmed;
67+
} catch {
68+
// SKILL.md not found under this hash entry, try next
69+
continue;
6270
}
71+
}
72+
73+
return undefined;
74+
}
75+
76+
/**
77+
* Fetch a skill from the factory-plugins GitHub repo.
78+
* Used as fallback when the local plugin cache is not available (e.g. CI).
79+
*/
80+
async function loadSkillFromGitHub(
81+
skillName: string,
82+
): Promise<string | undefined> {
83+
const url = `https://raw.githubusercontent.com/${PLUGINS_REPO}/${PLUGINS_BRANCH}/plugins/core/skills/${skillName}/SKILL.md`;
84+
try {
85+
const response = await fetch(url);
86+
if (!response.ok) return undefined;
87+
const content = await response.text();
88+
const trimmed = content.trim();
89+
if (!trimmed) return undefined;
90+
console.log(
91+
`Loaded skill ${skillName} from GitHub (${trimmed.length} bytes)`,
92+
);
93+
return trimmed;
6394
} catch {
64-
// Cache dir doesn't exist
95+
return undefined;
6596
}
97+
}
98+
99+
/**
100+
* Load a skill by name. Tries the local plugin cache first,
101+
* then falls back to fetching from the factory-plugins GitHub repo.
102+
*/
103+
export async function loadSkill(
104+
skillName: string,
105+
): Promise<string | undefined> {
106+
const cached = await loadSkillFromCache(skillName);
107+
if (cached) return cached;
108+
109+
const remote = await loadSkillFromGitHub(skillName);
110+
if (remote) return remote;
66111

67-
console.log(
68-
`Skill ${skillName} not found in core plugin cache. Ensure the Droid CLI is installed.`,
69-
);
112+
console.log(`Skill ${skillName} not found locally or on GitHub.`);
70113
return undefined;
71114
}

0 commit comments

Comments
 (0)