fix(ai-review 同步): 限制自動提交只包含問題檔 #18
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
name: triage-findings
|
|
||||||
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Triage Findings
|
|
||||||
|
|
||||||
## When To Use
|
|
||||||
|
|
||||||
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
|
||||||
It is also used when some findings are false positives and should be moved into the exclusions list.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Collect all findings into one list.
|
|
||||||
2. Merge duplicates into a single finding when they describe the same issue.
|
|
||||||
3. Sort the final list by severity:
|
|
||||||
- critical
|
|
||||||
- warning
|
|
||||||
- info
|
|
||||||
4. Renumber the sorted list from 1 upward.
|
|
||||||
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
|
||||||
6. If a finding is a false positive, do not keep it in the final list.
|
|
||||||
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
|
||||||
|
|
||||||
## Resolution Flow
|
|
||||||
|
|
||||||
After the list is merged and ordered, resolve the remaining findings one by one.
|
|
||||||
|
|
||||||
1. Start from the highest severity item.
|
|
||||||
2. Identify the root cause in the relevant file or context.
|
|
||||||
3. Apply the smallest safe change that fixes the issue.
|
|
||||||
4. Add or update tests when behavior changes.
|
|
||||||
5. Re-check the issue after the change.
|
|
||||||
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
|
||||||
7. Continue until the list is either fixed or explicitly excluded.
|
|
||||||
|
|
||||||
## Output Rules
|
|
||||||
|
|
||||||
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
|
||||||
- Keep numbering contiguous after filtering and merging.
|
|
||||||
- Preserve useful details like file path, location, and suggested fix.
|
|
||||||
- Keep exclusions entries minimal and consistent with the project schema.
|
|
||||||
- When writing exclusions, always output a top-level JSON array.
|
|
||||||
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
|
||||||
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# Triage Findings
|
|
||||||
|
|
||||||
## When To Use
|
|
||||||
|
|
||||||
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
|
||||||
It is also used when some findings are false positives and should be moved into the exclusions list.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Collect all findings into one list.
|
|
||||||
2. Merge duplicates into a single finding when they describe the same issue.
|
|
||||||
3. Sort the final list by severity:
|
|
||||||
- critical
|
|
||||||
- warning
|
|
||||||
- info
|
|
||||||
4. Renumber the sorted list from 1 upward.
|
|
||||||
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
|
||||||
6. If a finding is a false positive, do not keep it in the final list.
|
|
||||||
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
|
||||||
|
|
||||||
## Resolution Flow
|
|
||||||
|
|
||||||
After the list is merged and ordered, resolve the remaining findings one by one.
|
|
||||||
|
|
||||||
1. Start from the highest severity item.
|
|
||||||
2. Identify the root cause in the relevant file or context.
|
|
||||||
3. Apply the smallest safe change that fixes the issue.
|
|
||||||
4. Add or update tests when behavior changes.
|
|
||||||
5. Re-check the issue after the change.
|
|
||||||
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
|
||||||
7. Continue until the list is either fixed or explicitly excluded.
|
|
||||||
|
|
||||||
## Output Rules
|
|
||||||
|
|
||||||
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
|
||||||
- Keep numbering contiguous after filtering and merging.
|
|
||||||
- Preserve useful details like file path, location, and suggested fix.
|
|
||||||
- Keep exclusions entries minimal and consistent with the project schema.
|
|
||||||
- When writing exclusions, always output a top-level JSON array.
|
|
||||||
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
|
||||||
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
name: triage-findings
|
|
||||||
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Triage Findings
|
|
||||||
|
|
||||||
## When To Use
|
|
||||||
|
|
||||||
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
|
||||||
It is also used when some findings are false positives and should be moved into the exclusions list.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Collect all findings into one list.
|
|
||||||
2. Merge duplicates into a single finding when they describe the same issue.
|
|
||||||
3. Sort the final list by severity:
|
|
||||||
- critical
|
|
||||||
- warning
|
|
||||||
- info
|
|
||||||
4. Renumber the sorted list from 1 upward.
|
|
||||||
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
|
||||||
6. If a finding is a false positive, do not keep it in the final list.
|
|
||||||
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
|
||||||
|
|
||||||
## Resolution Flow
|
|
||||||
|
|
||||||
After the list is merged and ordered, resolve the remaining findings one by one.
|
|
||||||
|
|
||||||
1. Start from the highest severity item.
|
|
||||||
2. Identify the root cause in the relevant file or context.
|
|
||||||
3. Apply the smallest safe change that fixes the issue.
|
|
||||||
4. Add or update tests when behavior changes.
|
|
||||||
5. Re-check the issue after the change.
|
|
||||||
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
|
||||||
7. Continue until the list is either fixed or explicitly excluded.
|
|
||||||
|
|
||||||
## Output Rules
|
|
||||||
|
|
||||||
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
|
||||||
- Keep numbering contiguous after filtering and merging.
|
|
||||||
- Preserve useful details like file path, location, and suggested fix.
|
|
||||||
- Keep exclusions entries minimal and consistent with the project schema.
|
|
||||||
- When writing exclusions, always output a top-level JSON array.
|
|
||||||
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
|
||||||
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
name: triage-findings
|
|
||||||
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Triage Findings
|
|
||||||
|
|
||||||
## When To Use
|
|
||||||
|
|
||||||
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
|
||||||
It is also used when some findings are false positives and should be moved into the exclusions list.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Collect all findings into one list.
|
|
||||||
2. Merge duplicates into a single finding when they describe the same issue.
|
|
||||||
3. Sort the final list by severity:
|
|
||||||
- critical
|
|
||||||
- warning
|
|
||||||
- info
|
|
||||||
4. Renumber the sorted list from 1 upward.
|
|
||||||
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
|
||||||
6. If a finding is a false positive, do not keep it in the final list.
|
|
||||||
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
|
||||||
|
|
||||||
## Resolution Flow
|
|
||||||
|
|
||||||
After the list is merged and ordered, resolve the remaining findings one by one.
|
|
||||||
|
|
||||||
1. Start from the highest severity item.
|
|
||||||
2. Identify the root cause in the relevant file or context.
|
|
||||||
3. Apply the smallest safe change that fixes the issue.
|
|
||||||
4. Add or update tests when behavior changes.
|
|
||||||
5. Re-check the issue after the change.
|
|
||||||
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
|
||||||
7. Continue until the list is either fixed or explicitly excluded.
|
|
||||||
|
|
||||||
## Output Rules
|
|
||||||
|
|
||||||
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
|
||||||
- Keep numbering contiguous after filtering and merging.
|
|
||||||
- Preserve useful details like file path, location, and suggested fix.
|
|
||||||
- Keep exclusions entries minimal and consistent with the project schema.
|
|
||||||
- When writing exclusions, always output a top-level JSON array.
|
|
||||||
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
|
||||||
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
name: triage-findings
|
|
||||||
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Triage Findings
|
|
||||||
|
|
||||||
## When To Use
|
|
||||||
|
|
||||||
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
|
||||||
It is also used when some findings are false positives and should be moved into the exclusions list.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Collect all findings into one list.
|
|
||||||
2. Merge duplicates into a single finding when they describe the same issue.
|
|
||||||
3. Sort the final list by severity:
|
|
||||||
- critical
|
|
||||||
- warning
|
|
||||||
- info
|
|
||||||
4. Renumber the sorted list from 1 upward.
|
|
||||||
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
|
||||||
6. If a finding is a false positive, do not keep it in the final list.
|
|
||||||
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
|
||||||
|
|
||||||
## Resolution Flow
|
|
||||||
|
|
||||||
After the list is merged and ordered, resolve the remaining findings one by one.
|
|
||||||
|
|
||||||
1. Start from the highest severity item.
|
|
||||||
2. Identify the root cause in the relevant file or context.
|
|
||||||
3. Apply the smallest safe change that fixes the issue.
|
|
||||||
4. Add or update tests when behavior changes.
|
|
||||||
5. Re-check the issue after the change.
|
|
||||||
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
|
||||||
7. Continue until the list is either fixed or explicitly excluded.
|
|
||||||
|
|
||||||
## Output Rules
|
|
||||||
|
|
||||||
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
|
||||||
- Keep numbering contiguous after filtering and merging.
|
|
||||||
- Preserve useful details like file path, location, and suggested fix.
|
|
||||||
- Keep exclusions entries minimal and consistent with the project schema.
|
|
||||||
- When writing exclusions, always output a top-level JSON array.
|
|
||||||
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
|
||||||
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "Triage Findings"
|
|
||||||
short_description: "Triage, sort, fix, and exclude review findings"
|
|
||||||
default_prompt: "Use $triage-findings to merge review findings, sort and renumber them by severity, resolve real issues one by one, and add false positives to `.gitea/ai-review/exclusions.json` as a top-level JSON array."
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
name: triage-findings
|
|
||||||
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Triage Findings
|
|
||||||
|
|
||||||
## When To Use
|
|
||||||
|
|
||||||
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
|
||||||
It is also used when some findings are false positives and should be moved into the exclusions list.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Collect all findings into one list.
|
|
||||||
2. Merge duplicates into a single finding when they describe the same issue.
|
|
||||||
3. Sort the final list by severity:
|
|
||||||
- critical
|
|
||||||
- warning
|
|
||||||
- info
|
|
||||||
4. Renumber the sorted list from 1 upward.
|
|
||||||
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
|
||||||
6. If a finding is a false positive, do not keep it in the final list.
|
|
||||||
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
|
||||||
|
|
||||||
## Resolution Flow
|
|
||||||
|
|
||||||
After the list is merged and ordered, resolve the remaining findings one by one.
|
|
||||||
|
|
||||||
1. Start from the highest severity item.
|
|
||||||
2. Identify the root cause in the relevant file or context.
|
|
||||||
3. Apply the smallest safe change that fixes the issue.
|
|
||||||
4. Add or update tests when behavior changes.
|
|
||||||
5. Re-check the issue after the change.
|
|
||||||
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
|
||||||
7. Continue until the list is either fixed or explicitly excluded.
|
|
||||||
|
|
||||||
## Output Rules
|
|
||||||
|
|
||||||
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
|
||||||
- Keep numbering contiguous after filtering and merging.
|
|
||||||
- Preserve useful details like file path, location, and suggested fix.
|
|
||||||
- Keep exclusions entries minimal and consistent with the project schema.
|
|
||||||
- When writing exclusions, always output a top-level JSON array.
|
|
||||||
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
|
||||||
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Triage Findings
|
|
||||||
|
|
||||||
Use the triage-finding workflow for review issue lists:
|
|
||||||
|
|
||||||
1. Merge findings into one list.
|
|
||||||
2. Remove duplicates.
|
|
||||||
3. Sort by severity: `critical` -> `warning` -> `info`.
|
|
||||||
4. Renumber from 1.
|
|
||||||
5. Fix real issues with the smallest safe change.
|
|
||||||
6. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
|
|
||||||
7. Add or update tests when behavior changes.
|
|
||||||
8. Re-check after each fix.
|
|
||||||
|
|
||||||
The full reusable skill lives in `.github/skills/triage-findings/SKILL.md`.
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# Triage Findings
|
|
||||||
|
|
||||||
## When To Use
|
|
||||||
|
|
||||||
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
|
||||||
It is also used when some findings are false positives and should be moved into the exclusions list.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Collect all findings into one list.
|
|
||||||
2. Merge duplicates into a single finding when they describe the same issue.
|
|
||||||
3. Sort the final list by severity:
|
|
||||||
- critical
|
|
||||||
- warning
|
|
||||||
- info
|
|
||||||
4. Renumber the sorted list from 1 upward.
|
|
||||||
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
|
||||||
6. If a finding is a false positive, do not keep it in the final list.
|
|
||||||
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
|
||||||
|
|
||||||
## Resolution Flow
|
|
||||||
|
|
||||||
After the list is merged and ordered, resolve the remaining findings one by one.
|
|
||||||
|
|
||||||
1. Start from the highest severity item.
|
|
||||||
2. Identify the root cause in the relevant file or context.
|
|
||||||
3. Apply the smallest safe change that fixes the issue.
|
|
||||||
4. Add or update tests when behavior changes.
|
|
||||||
5. Re-check the issue after the change.
|
|
||||||
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
|
||||||
7. Continue until the list is either fixed or explicitly excluded.
|
|
||||||
|
|
||||||
## Output Rules
|
|
||||||
|
|
||||||
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
|
||||||
- Keep numbering contiguous after filtering and merging.
|
|
||||||
- Preserve useful details like file path, location, and suggested fix.
|
|
||||||
- Keep exclusions entries minimal and consistent with the project schema.
|
|
||||||
- When writing exclusions, always output a top-level JSON array.
|
|
||||||
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
|
||||||
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
|
||||||
-10
@@ -10,16 +10,6 @@ WORKDIR /action
|
|||||||
COPY app/package.json /action/app/
|
COPY app/package.json /action/app/
|
||||||
RUN cd /action/app && npm install
|
RUN cd /action/app && npm install
|
||||||
|
|
||||||
COPY .amazonq/ /action/.amazonq/
|
|
||||||
COPY .codex/ /action/.codex/
|
|
||||||
COPY .agents/ /action/.agents/
|
|
||||||
COPY .claude/ /action/.claude/
|
|
||||||
COPY .gemini/ /action/.gemini/
|
|
||||||
COPY .github/ /action/.github/
|
|
||||||
COPY AGENTS.md /action/
|
|
||||||
COPY CLAUDE.md /action/
|
|
||||||
COPY GEMINI.md /action/
|
|
||||||
|
|
||||||
COPY app/ /action/app/
|
COPY app/ /action/app/
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|||||||
+9
-246
@@ -1,51 +1,12 @@
|
|||||||
import { spawnSync } from 'child_process';
|
import { spawnSync } from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js';
|
||||||
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH, getLLMConfig } from './config.js';
|
import { line, ok, warn } from './log.js';
|
||||||
import { line, ok, warn, error } from './log.js';
|
|
||||||
|
|
||||||
const ACTION_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const REVIEW_FILE_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json'];
|
||||||
const GENERATED_SYNC_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json'];
|
|
||||||
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
|
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
|
||||||
export const BOT_COMMIT_MARKER = '[ai-review-bot]';
|
export const BOT_COMMIT_MARKER = '[ai-review-bot]';
|
||||||
export const SYNC_PATHS = [
|
|
||||||
'.amazonq/rules/triage-findings.md',
|
|
||||||
'.agents/skills/triage-findings/SKILL.md',
|
|
||||||
'.antigravity/skills/triage-findings/SKILL.md',
|
|
||||||
'.codex/skills/triage-findings/SKILL.md',
|
|
||||||
'.codex/skills/triage-findings/agents/openai.yaml',
|
|
||||||
'.claude/skills/triage-findings/SKILL.md',
|
|
||||||
'.gemini/skills/triage-findings/SKILL.md',
|
|
||||||
'.github/copilot-instructions.md',
|
|
||||||
'.github/skills/triage-findings/SKILL.md',
|
|
||||||
'AGENTS.md',
|
|
||||||
'ANTIGRAVITY.md',
|
|
||||||
'CLAUDE.md',
|
|
||||||
'GEMINI.md',
|
|
||||||
];
|
|
||||||
const FORCE_SYNC_FILE_PATHS = [
|
|
||||||
'.github/copilot-instructions.md',
|
|
||||||
'AGENTS.md',
|
|
||||||
'ANTIGRAVITY.md',
|
|
||||||
'CLAUDE.md',
|
|
||||||
'GEMINI.md',
|
|
||||||
];
|
|
||||||
const MERGE_SYNC_FILE_PATHS = new Set([
|
|
||||||
'AGENTS.md',
|
|
||||||
'ANTIGRAVITY.md',
|
|
||||||
'CLAUDE.md',
|
|
||||||
'GEMINI.md',
|
|
||||||
]);
|
|
||||||
let instructionMergeAssistantPromise = null;
|
|
||||||
const SYNC_TREE_PATHS = [
|
|
||||||
'.agents/skills/triage-findings',
|
|
||||||
'.antigravity/skills/triage-findings',
|
|
||||||
'.codex/skills/triage-findings',
|
|
||||||
'.claude/skills/triage-findings',
|
|
||||||
'.gemini/skills/triage-findings',
|
|
||||||
'.github/skills/triage-findings',
|
|
||||||
];
|
|
||||||
|
|
||||||
function makeRunner(spawn) {
|
function makeRunner(spawn) {
|
||||||
return function run(args, cwd, env) {
|
return function run(args, cwd, env) {
|
||||||
@@ -88,173 +49,6 @@ function readGitOutput(run, args, cwd, env) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeText(text) {
|
|
||||||
return text.replace(/\r\n/g, '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitTextBlocks(text) {
|
|
||||||
const normalized = normalizeText(text).replace(/\n+$/, '');
|
|
||||||
if (!normalized) return [];
|
|
||||||
return normalized.split(/\n{2,}/).map(block => block.trimEnd()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeText(existingText, sourceText) {
|
|
||||||
const existing = normalizeText(existingText);
|
|
||||||
const source = normalizeText(sourceText);
|
|
||||||
if (existing === source) return existing;
|
|
||||||
|
|
||||||
const mergedBlocks = splitTextBlocks(existing);
|
|
||||||
const seenBlocks = new Set(mergedBlocks.map(block => block.trim()));
|
|
||||||
let changed = false;
|
|
||||||
|
|
||||||
for (const block of splitTextBlocks(source)) {
|
|
||||||
const key = block.trim();
|
|
||||||
if (seenBlocks.has(key)) continue;
|
|
||||||
seenBlocks.add(key);
|
|
||||||
mergedBlocks.push(block);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!changed) return existing;
|
|
||||||
return `${mergedBlocks.join('\n\n')}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniqueBlocksFromTexts(...texts) {
|
|
||||||
const seen = new Set();
|
|
||||||
const blocks = [];
|
|
||||||
for (const text of texts) {
|
|
||||||
for (const block of splitTextBlocks(text)) {
|
|
||||||
const key = block.trim();
|
|
||||||
if (!key || seen.has(key)) continue;
|
|
||||||
seen.add(key);
|
|
||||||
blocks.push(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateMergedInstructionText(mergedText, requiredBlocks) {
|
|
||||||
const candidate = normalizeText(mergedText);
|
|
||||||
return requiredBlocks.every(block => candidate.includes(normalizeText(block).trim()));
|
|
||||||
}
|
|
||||||
|
|
||||||
class InstructionMergeError extends Error {
|
|
||||||
constructor(message, options) {
|
|
||||||
super(message, options);
|
|
||||||
this.name = 'InstructionMergeError';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function abortInstructionMerge(message) {
|
|
||||||
error(message);
|
|
||||||
process.exit(1);
|
|
||||||
throw new InstructionMergeError(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncFileOverwrite(sourceRoot, repoDir, relPath) {
|
|
||||||
const src = path.join(sourceRoot, relPath);
|
|
||||||
if (!fs.existsSync(src)) return null;
|
|
||||||
|
|
||||||
const dest = path.join(repoDir, relPath);
|
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
return relPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getInstructionMergeAssistant() {
|
|
||||||
const { provider } = getLLMConfig();
|
|
||||||
if (!provider) return null;
|
|
||||||
if (instructionMergeAssistantPromise) return instructionMergeAssistantPromise;
|
|
||||||
|
|
||||||
instructionMergeAssistantPromise = (async () => {
|
|
||||||
try {
|
|
||||||
const { chatJSON } = await import('./llm.js');
|
|
||||||
return async ({ relPath, existingText, sourceText, deterministicText }) => {
|
|
||||||
const systemPrompt = [
|
|
||||||
'You merge repository instruction files without losing any skill, command, or rule.',
|
|
||||||
'Never delete unique content from either input.',
|
|
||||||
'You may only remove exact duplicates or improve ordering/formatting.',
|
|
||||||
'Return JSON with a single field: merged_text.',
|
|
||||||
].join(' ');
|
|
||||||
const userContent = JSON.stringify({
|
|
||||||
path: relPath,
|
|
||||||
existing_text: existingText,
|
|
||||||
source_text: sourceText,
|
|
||||||
deterministic_candidate: deterministicText,
|
|
||||||
});
|
|
||||||
const result = await chatJSON(systemPrompt, userContent);
|
|
||||||
if (typeof result === 'string') return result;
|
|
||||||
if (result && typeof result.merged_text === 'string') return result.merged_text;
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
warn(`[merge] AI instruction merge unavailable: ${e.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return instructionMergeAssistantPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mergeInstructionText(existingText, sourceText, relPath, aiMergeAssistant = null) {
|
|
||||||
const deterministic = mergeText(existingText, sourceText);
|
|
||||||
const requiredBlocks = uniqueBlocksFromTexts(existingText, sourceText);
|
|
||||||
if (!aiMergeAssistant || requiredBlocks.length === 0) return deterministic;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const aiMerged = await aiMergeAssistant({ relPath, existingText, sourceText, deterministicText: deterministic, requiredBlocks });
|
|
||||||
if (aiMerged == null) {
|
|
||||||
warn(`[merge] ${relPath} AI result unavailable; using deterministic merge`);
|
|
||||||
return deterministic;
|
|
||||||
}
|
|
||||||
if (typeof aiMerged === 'string' && validateMergedInstructionText(aiMerged, requiredBlocks)) {
|
|
||||||
return normalizeText(aiMerged) === normalizeText(existingText) ? existingText : aiMerged;
|
|
||||||
}
|
|
||||||
abortInstructionMerge(`[merge] ${relPath} AI result rejected; refusing fallback`);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof InstructionMergeError) throw e;
|
|
||||||
abortInstructionMerge(`[merge] ${relPath} AI merge failed: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncInstructionFile(sourceRoot, repoDir, relPath, aiMergeAssistant = null) {
|
|
||||||
const src = path.join(sourceRoot, relPath);
|
|
||||||
if (!fs.existsSync(src)) return null;
|
|
||||||
|
|
||||||
const dest = path.join(repoDir, relPath);
|
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
||||||
|
|
||||||
if (!fs.existsSync(dest)) {
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
return relPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingText = fs.readFileSync(dest, 'utf8');
|
|
||||||
const sourceText = fs.readFileSync(src, 'utf8');
|
|
||||||
const merged = await mergeInstructionText(existingText, sourceText, relPath, aiMergeAssistant);
|
|
||||||
if (merged !== existingText) {
|
|
||||||
fs.writeFileSync(dest, merged, 'utf8');
|
|
||||||
}
|
|
||||||
return relPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncTree(sourceRoot, repoDir, relDir) {
|
|
||||||
const srcDir = path.join(sourceRoot, relDir);
|
|
||||||
if (!fs.existsSync(srcDir)) return [];
|
|
||||||
|
|
||||||
const copied = [];
|
|
||||||
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
||||||
const relPath = path.join(relDir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
copied.push(...syncTree(sourceRoot, repoDir, relPath));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const synced = syncFileOverwrite(sourceRoot, repoDir, relPath);
|
|
||||||
if (synced) copied.push(synced);
|
|
||||||
}
|
|
||||||
return copied;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRepoState(repoDir, _spawnSync = spawnSync) {
|
export function getRepoState(repoDir, _spawnSync = spawnSync) {
|
||||||
const run = makeRunner(_spawnSync);
|
const run = makeRunner(_spawnSync);
|
||||||
const headSha = readGitOutput(run, ['rev-parse', 'HEAD'], repoDir);
|
const headSha = readGitOutput(run, ['rev-parse', 'HEAD'], repoDir);
|
||||||
@@ -311,7 +105,7 @@ export function cloneRepo(workspace, _spawnSync = spawnSync) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync, sourceRoot = ACTION_ROOT, reviewOutcome = 'success') {
|
export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync, _sourceRoot = null, reviewOutcome = 'success') {
|
||||||
const run = makeRunner(_spawnSync);
|
const run = makeRunner(_spawnSync);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -323,51 +117,20 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync,
|
|||||||
run(['reset', '--hard', `origin/${PR_HEAD_BRANCH}`], repoDir);
|
run(['reset', '--hard', `origin/${PR_HEAD_BRANCH}`], repoDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingSyncPaths = new Set();
|
const reviewFilePaths = REVIEW_FILE_PATHS.filter(relPath => fs.existsSync(path.join(workspace, relPath)));
|
||||||
const aiMergeAssistant = await getInstructionMergeAssistant();
|
if (reviewFilePaths.length > 0) {
|
||||||
|
for (const relPath of reviewFilePaths) {
|
||||||
// Copy action skill trees into the target repo. Existing files are merged with
|
|
||||||
// the action source; missing source files are ignored so we do not delete
|
|
||||||
// target repo content.
|
|
||||||
for (const relDir of SYNC_TREE_PATHS) {
|
|
||||||
for (const relPath of syncTree(sourceRoot, repoDir, relDir)) {
|
|
||||||
existingSyncPaths.add(relPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge only the direct instruction files that must preserve repository-specific
|
|
||||||
// skills, commands, and rules. Everything else keeps the source copy.
|
|
||||||
for (const relPath of FORCE_SYNC_FILE_PATHS) {
|
|
||||||
const copied = MERGE_SYNC_FILE_PATHS.has(relPath)
|
|
||||||
? await syncInstructionFile(sourceRoot, repoDir, relPath, aiMergeAssistant)
|
|
||||||
: syncFileOverwrite(sourceRoot, repoDir, relPath);
|
|
||||||
if (copied) existingSyncPaths.add(copied);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge standalone action files into the target repo.
|
|
||||||
for (const relPath of SYNC_PATHS) {
|
|
||||||
if (FORCE_SYNC_FILE_PATHS.includes(relPath)) continue;
|
|
||||||
const copied = syncFileOverwrite(sourceRoot, repoDir, relPath);
|
|
||||||
if (copied) existingSyncPaths.add(copied);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingSyncPaths.size > 0) {
|
|
||||||
run(['add', ...existingSyncPaths], repoDir);
|
|
||||||
}
|
|
||||||
const generatedSyncPaths = GENERATED_SYNC_PATHS.filter(relPath => fs.existsSync(path.join(workspace, relPath)));
|
|
||||||
if (generatedSyncPaths.length > 0) {
|
|
||||||
for (const relPath of generatedSyncPaths) {
|
|
||||||
const src = path.join(workspace, relPath);
|
const src = path.join(workspace, relPath);
|
||||||
const dest = path.join(repoDir, relPath);
|
const dest = path.join(repoDir, relPath);
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
fs.copyFileSync(src, dest);
|
fs.copyFileSync(src, dest);
|
||||||
}
|
}
|
||||||
run(['add', ...generatedSyncPaths], repoDir);
|
run(['add', ...reviewFilePaths], repoDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = run(['status', '--porcelain'], repoDir);
|
const status = run(['status', '--porcelain'], repoDir);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
line('sync files 無變更,跳過 commit');
|
line('review files 無變更,跳過 commit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,18 +25,8 @@ export function getBotReviewOutcome(message) {
|
|||||||
export async function getPRDiff() {
|
export async function getPRDiff() {
|
||||||
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent });
|
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent });
|
||||||
return filterDiff(resp.data, [
|
return filterDiff(resp.data, [
|
||||||
'.amazonq/',
|
|
||||||
'.agents/',
|
|
||||||
'.antigravity/',
|
|
||||||
'.claude/',
|
|
||||||
'.codex/',
|
|
||||||
'.gemini/',
|
|
||||||
'.gitea/',
|
'.gitea/',
|
||||||
'.github/',
|
'.github/',
|
||||||
'AGENTS.md',
|
|
||||||
'ANTIGRAVITY.md',
|
|
||||||
'CLAUDE.md',
|
|
||||||
'GEMINI.md',
|
|
||||||
'README.md',
|
'README.md',
|
||||||
'TODO.md',
|
'TODO.md',
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user