@godv61/dsh-task-engine 0.29.3 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.adaptive-test.mjs +380 -185
- package/.codex-project-test.mjs +18 -18
- package/.evidence-test.mjs +14 -1
- package/.hook-test.mjs +19 -19
- package/.resource-test.mjs +17 -0
- package/.roundtrip-test.mjs +8 -3
- package/.sonar-credential-test.mjs +38 -0
- package/.sonarlint-local-test.mjs +71 -0
- package/.workflow-test.mjs +56 -9
- package/README.md +35 -112
- package/docs/development.md +45 -53
- package/docs/manual.html +140 -84
- package/hooks/commit-msg +19 -1
- package/lib/adaptive.js +1 -1
- package/lib/adaptive.js.map +1 -1
- package/lib/client.js +297 -9
- package/lib/client.js.map +1 -1
- package/lib/controller.d.ts +44 -0
- package/lib/controller.js +73 -0
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.d.ts +2 -0
- package/lib/dev-task.js +342 -42
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +5 -1
- package/lib/engine.js +6 -0
- package/lib/engine.js.map +1 -1
- package/lib/hook.js +3 -3
- package/lib/hook.js.map +1 -1
- package/lib/project-init.d.ts +4 -0
- package/lib/project-init.js +21 -2
- package/lib/project-init.js.map +1 -1
- package/lib/skill-audit.js +9 -1
- package/lib/skill-audit.js.map +1 -1
- package/lib/sonar-credential.d.ts +17 -0
- package/lib/sonar-credential.js +17 -0
- package/lib/sonar-credential.js.map +1 -0
- package/lib/sonar-report.d.ts +4 -0
- package/lib/sonar-report.js +53 -0
- package/lib/sonar-report.js.map +1 -0
- package/lib/sonar.d.ts +35 -2
- package/lib/sonar.js +63 -4
- package/lib/sonar.js.map +1 -1
- package/lib/sonarlint-local.d.ts +17 -0
- package/lib/sonarlint-local.js +317 -0
- package/lib/sonarlint-local.js.map +1 -0
- package/lib/verification-tests.d.ts +10 -0
- package/lib/verification-tests.js +15 -0
- package/lib/verification-tests.js.map +1 -0
- package/package.json +6 -4
- package/preset/enable.mjs +2 -2
- package/preset/persona.md +4 -4
- package/scripts/verify-dsh-compat.mjs +14 -14
- package/scripts/verify-package.mjs +10 -8
- package/skills/architecture-design/SKILL.md +11 -11
- package/skills/code-development/SKILL.md +11 -11
- package/skills/code-review/SKILL.md +12 -10
- package/skills/eng-delivery/SKILL.md +10 -7
- package/skills/requirements-analysis/SKILL.md +11 -11
- package/skills/task-orchestration/SKILL.md +11 -11
- package/skills/test-validation/SKILL.md +11 -9
- package/docs/BRIEF-FOR-REVIEW.md +0 -163
- package/docs/CHANGELOG.md +0 -407
- package/docs/README.md +0 -42
- package/docs/adaptive-workflows.md +0 -76
- package/docs/assets/workflow-banner.svg +0 -29
- package/docs/configuration.md +0 -80
- package/docs/faq.md +0 -59
- package/docs/getting-started.md +0 -55
- package/docs/listing/godv61__dsh-task-engine.yml +0 -6
- package/docs/listing/submission.md +0 -84
- package/docs/manual-legacy.html +0 -380
- package/docs/releases/0.23.0.md +0 -32
- package/docs/releases/0.23.1.md +0 -58
- package/docs/releases/0.23.2.md +0 -21
- package/docs/resource-install.md +0 -64
- package/docs/roadmap.md +0 -33
- package/docs/testing/0.23.0//346/265/213/350/257/225/346/211/247/350/241/214/350/256/260/345/275/225.md +0 -189
- package/docs/testing/0.23.0//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -42
- package/docs/testing/0.23.1//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -34
- package/docs/testing/0.23.1//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -41
- package/docs/testing/0.23.2/R02/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -56
- package/docs/testing/0.23.2/R03/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -38
- package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -65
- package/docs/testing/0.23.2//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -43
- package/docs/workflow-regression.md +0 -36
package/lib/dev-task.js
CHANGED
|
@@ -10,18 +10,22 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module dsh-task-engine/dev-task
|
|
12
12
|
*/
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
14
|
-
import { homedir } from 'node:os';
|
|
13
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
14
|
+
import { homedir, tmpdir } from 'node:os';
|
|
15
15
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
18
18
|
import { assertAdvance, bindingsForStage, checkFileScope, commitCheckpoint, applyRevision, completionBlockers, formatResourceRef, invalidatedBy, legalTargets, resourceBlockers, newTask, taskIdFromMessage, unmetGuards, validateCommitMessage, validateWorkflow, verificationBlockers, } from "./engine.js";
|
|
19
19
|
import { HIGH_RISK_REQUIRED_CAPABILITIES, flowSatisfies, resolveFlow, } from "./workflows.js";
|
|
20
20
|
import { ADAPTIVE_VERSION, COMPLEXITY_OPTIONS, META_STAGES, adaptiveWorkflow, isComplexity, metaForStage } from "./adaptive.js";
|
|
21
|
-
import { inspectSonar } from "./sonar.js";
|
|
22
|
-
import {
|
|
21
|
+
import { assertLocalSonarReady, inspectSonar, localReviewGate, unresolvedBlockingFindings, validAuditIncludePaths, validLocalScanCommand } from "./sonar.js";
|
|
22
|
+
import { inspectLocalRules } from "./sonarlint-local.js";
|
|
23
|
+
import { renderSonarReport, sonarReportPath } from "./sonar-report.js";
|
|
24
|
+
import { resolveSonarToken } from "./sonar-credential.js";
|
|
25
|
+
import { projectMapCoverageGaps, projectMapCoveragePaths, scanProject } from "./project-init.js";
|
|
23
26
|
import { hashConfig, hashText } from "./snapshot.js";
|
|
24
27
|
import { withUserSkillProfiles } from "./user-skill-profiles.js";
|
|
28
|
+
import { mavenTestEvidence } from "./verification-tests.js";
|
|
25
29
|
import { loadedSkills, needsSkillReceipt, obligationStages, skillBlockers } from "./skill-audit.js";
|
|
26
30
|
import { detectRoot, detectType, defaultVerifyCommand, listProjectRules, listProjectSkills, presentGovernanceFiles, } from "./project.js";
|
|
27
31
|
/** Bundled commit-msg hook, copied into `.git/hooks/` by the `install_hook` operation. */
|
|
@@ -101,6 +105,13 @@ async function resolveVerifyCommand(fs, cwd) {
|
|
|
101
105
|
}
|
|
102
106
|
/** Hard cap on a verification command's run time; a timeout marks the receipt non-passing. */
|
|
103
107
|
const VERIFY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
108
|
+
async function runShell(shell, spec) {
|
|
109
|
+
if (typeof shell.execute === 'function')
|
|
110
|
+
return (await shell.execute(spec)).result();
|
|
111
|
+
if (typeof shell.run === 'function')
|
|
112
|
+
return shell.run(spec);
|
|
113
|
+
throw new Error('host shell service has neither execute nor run');
|
|
114
|
+
}
|
|
104
115
|
/**
|
|
105
116
|
* Run a verification command through the host shell service and return an
|
|
106
117
|
* objective {@link VerificationReceipt}; `passed` derives from the exit code,
|
|
@@ -122,7 +133,7 @@ async function runVerificationCommand(ctx, command, cwd, exec, approvedMode) {
|
|
|
122
133
|
timeoutMs: VERIFY_TIMEOUT_MS, signal: exec.signal,
|
|
123
134
|
...(sandboxPolicy ? { sandboxPolicy } : {}),
|
|
124
135
|
});
|
|
125
|
-
const outcome = await shell
|
|
136
|
+
const outcome = await runShell(shell, spec);
|
|
126
137
|
return {
|
|
127
138
|
command,
|
|
128
139
|
exit_code: outcome.exitCode ?? -1,
|
|
@@ -136,6 +147,55 @@ async function runVerificationCommand(ctx, command, cwd, exec, approvedMode) {
|
|
|
136
147
|
...(outcome.sandbox ? { sandbox: outcome.sandbox } : {}),
|
|
137
148
|
};
|
|
138
149
|
}
|
|
150
|
+
/** Run a local scanner without putting the credential on a command line or in the task record. */
|
|
151
|
+
async function runLocalSonar(ctx, policy, token, root, branch, exec, defaultCommand, approvedMode) {
|
|
152
|
+
if (!token)
|
|
153
|
+
throw new Error('SonarQube Token is not configured for this project');
|
|
154
|
+
if (!/^[A-Za-z0-9._/-]+$/u.test(branch) || !policy.reference_branch
|
|
155
|
+
|| branch === policy.reference_branch || !/^[A-Za-z0-9._/-]+$/u.test(policy.reference_branch)) {
|
|
156
|
+
throw new Error('local SonarQube scan needs a safe current branch and a different reference branch');
|
|
157
|
+
}
|
|
158
|
+
if (!/^[A-Za-z0-9_.:-]+$/u.test(policy.project_key))
|
|
159
|
+
throw new Error('invalid SonarQube project key');
|
|
160
|
+
const shell = ctx.get('shell');
|
|
161
|
+
if (!shell)
|
|
162
|
+
throw new Error('local SonarQube scan needs the host shell service');
|
|
163
|
+
const temporary = mkdtempSync(join(tmpdir(), 'dsh-sonar-'));
|
|
164
|
+
try {
|
|
165
|
+
const metadata = join(temporary, 'report-task.txt');
|
|
166
|
+
const quote = (value) => `'${value.replace(/'/gu, "''")}'`;
|
|
167
|
+
const command = [policy.scan_command?.trim() || defaultCommand,
|
|
168
|
+
`-Dsonar.projectKey=${quote(policy.project_key)}`,
|
|
169
|
+
`-Dsonar.branch.name=${quote(branch)}`,
|
|
170
|
+
`-Dsonar.newCode.referenceBranch=${quote(policy.reference_branch)}`,
|
|
171
|
+
`-Dsonar.scanner.metadataFilePath=${quote(metadata)}`,
|
|
172
|
+
'-Dsonar.qualitygate.wait=false'].join(' ');
|
|
173
|
+
const policyService = ctx.get('sandboxPolicy');
|
|
174
|
+
const standingPolicy = policyService?.resolve({ session: exec.agent?.session });
|
|
175
|
+
const sandboxPolicy = approvedMode === undefined ? standingPolicy : { ...standingPolicy, mode: approvedMode };
|
|
176
|
+
const spec = shell.resolve({ command, workdir: root, timeoutMs: 30 * 60 * 1000, signal: exec.signal,
|
|
177
|
+
env: { SONAR_TOKEN: token, SONAR_HOST_URL: policy.host_url },
|
|
178
|
+
...(sandboxPolicy ? { sandboxPolicy } : {}) });
|
|
179
|
+
const outcome = await runShell(shell, spec);
|
|
180
|
+
if (outcome.exitCode !== 0 || outcome.aborted || outcome.timedOut || outcome.sandbox?.denied || outcome.sandbox?.runnerFailed) {
|
|
181
|
+
throw new Error(`local SonarQube scanner did not complete (exit ${outcome.exitCode ?? 'unknown'}); inspect scanner output and retry`);
|
|
182
|
+
}
|
|
183
|
+
let report;
|
|
184
|
+
try {
|
|
185
|
+
report = readFileSync(metadata, 'utf8');
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw new Error('local SonarQube scanner did not write report-task.txt; check scanner metadataFilePath support');
|
|
189
|
+
}
|
|
190
|
+
const id = /^ceTaskId=(.+)$/mu.exec(report)?.[1]?.trim();
|
|
191
|
+
if (!id || !/^[\w-]{8,120}$/u.test(id))
|
|
192
|
+
throw new Error('local SonarQube scanner returned an invalid ceTaskId');
|
|
193
|
+
return id;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
139
199
|
function sanitize(id) {
|
|
140
200
|
return id.replace(/[^0-9a-zA-Z_-]/g, '-');
|
|
141
201
|
}
|
|
@@ -462,7 +522,26 @@ function sonarPolicyFor(project) {
|
|
|
462
522
|
const mode = sonar.mode ?? 'branch';
|
|
463
523
|
if (mode !== 'branch' && mode !== 'pull-request')
|
|
464
524
|
throw new Error('sonar.mode must be branch or pull-request');
|
|
465
|
-
|
|
525
|
+
const source = sonar.source ?? 'ci';
|
|
526
|
+
if (source !== 'ci' && source !== 'local' && source !== 'ide-local')
|
|
527
|
+
throw new Error('sonar.source must be ci, local, or ide-local');
|
|
528
|
+
if ((source === 'local' || source === 'ide-local') && (mode !== 'branch' || !sonar.reference_branch?.trim())) {
|
|
529
|
+
throw new Error('local Sonar review needs branch mode and a reference_branch for new code');
|
|
530
|
+
}
|
|
531
|
+
if (sonar.reference_branch && !/^[A-Za-z0-9._/-]+$/u.test(sonar.reference_branch))
|
|
532
|
+
throw new Error('invalid SonarQube reference_branch');
|
|
533
|
+
if (sonar.scan_command && !validLocalScanCommand(sonar.scan_command)) {
|
|
534
|
+
throw new Error('scan_command must be a single Maven or SonarScanner command without shell operators or Token arguments');
|
|
535
|
+
}
|
|
536
|
+
if (sonar.include_paths !== undefined && !validAuditIncludePaths(sonar.include_paths)) {
|
|
537
|
+
throw new Error('sonar.include_paths must contain safe project-relative paths');
|
|
538
|
+
}
|
|
539
|
+
if (sonar.include_paths?.length && source !== 'ide-local')
|
|
540
|
+
throw new Error('sonar.include_paths only applies to ide-local reviews');
|
|
541
|
+
return { enabled: true, host_url, project_key, token_env, mode, source,
|
|
542
|
+
...(sonar.reference_branch ? { reference_branch: sonar.reference_branch } : {}),
|
|
543
|
+
...(sonar.scan_command ? { scan_command: sonar.scan_command } : {}),
|
|
544
|
+
...(sonar.include_paths?.length ? { include_paths: sonar.include_paths } : {}) };
|
|
466
545
|
}
|
|
467
546
|
async function resolveLogicalSkill(name, fs, cwd) {
|
|
468
547
|
if (!RESOURCE_NAME.test(name))
|
|
@@ -788,7 +867,7 @@ async function renderBindings(stage, workflow, fs, cwd, frozen) {
|
|
|
788
867
|
return [skillPart, skillInstructions, receiptPart, rulePart, missingPart, missingSkillPart, legacyPart].filter(Boolean).join('\n');
|
|
789
868
|
}
|
|
790
869
|
/** Normalize the tool's `items` argument into complete TaskItem records. */
|
|
791
|
-
function normalizeItems(items, previous = []) {
|
|
870
|
+
function normalizeItems(items, previous = [], mode = 'merge') {
|
|
792
871
|
const ids = new Set();
|
|
793
872
|
const result = (items ?? []).map(item => {
|
|
794
873
|
const id = String(item.id ?? '').trim();
|
|
@@ -804,9 +883,16 @@ function normalizeItems(items, previous = []) {
|
|
|
804
883
|
const unchanged = old?.title === title && !(old.status === 'done' && status !== 'done');
|
|
805
884
|
return { ...(unchanged ? old : {}), id, title, status };
|
|
806
885
|
});
|
|
807
|
-
|
|
886
|
+
// Incremental updates retain all prior items; an explicit replacement may remove only untouched work.
|
|
887
|
+
const merged = mode === 'merge'
|
|
888
|
+
? [...previous.map(item => result.find(next => next.id === item.id) ?? item),
|
|
889
|
+
...result.filter(item => !previous.some(old => old.id === item.id))]
|
|
890
|
+
: [...result, ...previous.filter(item => !ids.has(item.id)
|
|
891
|
+
&& (item.status === 'done' || item.dispatch !== undefined || item.review !== undefined))];
|
|
892
|
+
if (merged.filter(item => item.status === 'doing').length > 1) {
|
|
808
893
|
throw new Error('only one implementation item may be doing');
|
|
809
|
-
|
|
894
|
+
}
|
|
895
|
+
return merged;
|
|
810
896
|
}
|
|
811
897
|
/** Hash declared deliverables through host FS; task bookkeeping is excluded to avoid self-invalidation. */
|
|
812
898
|
async function scopeFingerprint(fs, state, cwd) {
|
|
@@ -843,7 +929,7 @@ async function evidenceBlockers(fs, state, workflow, cwd) {
|
|
|
843
929
|
if (receipt !== undefined && receipt.scope_hash !== current) {
|
|
844
930
|
blockers.push('verification is stale after file/scope changes; rerun verify with a real command');
|
|
845
931
|
}
|
|
846
|
-
else if (
|
|
932
|
+
else if (verificationBlockers(state, workflow).length > 0) {
|
|
847
933
|
// Only where the flow actually holds a verification requirement: a task that
|
|
848
934
|
// has not reached a verification gate yet has nothing to re-verify, and
|
|
849
935
|
// reporting it there would block the flow from starting at all.
|
|
@@ -899,16 +985,101 @@ async function sonarBlockers(fs, state, cwd) {
|
|
|
899
985
|
const current = await scopeFingerprint(fs, state, cwd);
|
|
900
986
|
const problems = [];
|
|
901
987
|
if (state.sonar_audit.scope_hash !== current)
|
|
902
|
-
problems.push('SonarQube audit is stale after file changes; rerun
|
|
988
|
+
problems.push('SonarQube audit is stale after file changes; rerun the scan and sonar_check');
|
|
903
989
|
const commit = state.commits.findLast(entry => entry.hash !== undefined)?.hash;
|
|
904
|
-
if (!commit || state.sonar_audit.commit_hash !== commit)
|
|
990
|
+
if (state.sonar_policy.source !== 'ide-local' && (!commit || state.sonar_audit.commit_hash !== commit)) {
|
|
905
991
|
problems.push('SonarQube audit does not match the task\'s latest recorded commit');
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
992
|
+
}
|
|
993
|
+
if (state.sonar_policy.source === 'ide-local') {
|
|
994
|
+
if (localReviewGate(state.sonar_audit) !== 'OK')
|
|
995
|
+
problems.push('本地规则审核尚有未解决问题或未覆盖文件');
|
|
996
|
+
const unresolved = unresolvedBlockingFindings(state.sonar_audit);
|
|
997
|
+
if (unresolved.length)
|
|
998
|
+
problems.push(`${unresolved.length} medium/high SonarQube new-code findings remain`);
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
if (state.sonar_audit.gate !== 'OK')
|
|
1002
|
+
problems.push(`SonarQube Quality Gate is ${state.sonar_audit.gate}`);
|
|
1003
|
+
if (state.sonar_audit.blocking.length)
|
|
1004
|
+
problems.push(`${state.sonar_audit.blocking.length} medium/high SonarQube new-code findings remain`);
|
|
1005
|
+
}
|
|
1006
|
+
if (state.sonar_audit.uncovered_files?.length)
|
|
1007
|
+
problems.push(`本地规则审核未覆盖 ${state.sonar_audit.uncovered_files.length} 个新增代码文件`);
|
|
910
1008
|
return problems;
|
|
911
1009
|
}
|
|
1010
|
+
/** Explicit item headings in a task plan, for checking plan/ledger consistency. */
|
|
1011
|
+
export function plannedItemIds(steps) {
|
|
1012
|
+
return [...new Set(steps.split(/\r?\n/u).flatMap(line => {
|
|
1013
|
+
const match = /^\s*(?:[-*+]\s+|#{1,6}\s+)?([A-Za-z][A-Za-z0-9_-]*\d+)\b/u.exec(line);
|
|
1014
|
+
return match === null ? [] : [match[1]];
|
|
1015
|
+
}))];
|
|
1016
|
+
}
|
|
1017
|
+
function taskPlanItemBlockers(state) {
|
|
1018
|
+
if (state.stage !== '任务编排' || state.complexity === undefined)
|
|
1019
|
+
return [];
|
|
1020
|
+
const ids = plannedItemIds(state.artifacts['task-plan']?.steps ?? '');
|
|
1021
|
+
if (!ids.length)
|
|
1022
|
+
return ['task-plan.steps must list implementation items on separate lines with stable IDs (for example, - I1 ...); use the same IDs in dev_task items'];
|
|
1023
|
+
const recorded = new Set(state.items.map(item => item.id));
|
|
1024
|
+
const missing = ids.filter(id => !recorded.has(id));
|
|
1025
|
+
const unplanned = state.items.filter(item => !ids.includes(item.id)).map(item => item.id);
|
|
1026
|
+
return [...(missing.length ? [`task plan items missing from ledger: ${missing.join(', ')}`] : []),
|
|
1027
|
+
...(unplanned.length ? [`ledger items absent from task plan: ${unplanned.join(', ')}`] : [])];
|
|
1028
|
+
}
|
|
1029
|
+
/** One optional, reviewable learning candidate per server rule, never an automatic Rule. */
|
|
1030
|
+
function ruleLearningCandidates(state) {
|
|
1031
|
+
const audits = [...(state.sonar_history ?? []), state.sonar_audit].filter((audit) => audit !== undefined);
|
|
1032
|
+
const learnedKeys = new Set((state.learned_rules ?? []).map(entry => entry.issue_key));
|
|
1033
|
+
const eligible = audits.flatMap(audit => unresolvedBlockingFindings(audit));
|
|
1034
|
+
const learnedRules = new Set(eligible
|
|
1035
|
+
.filter(issue => learnedKeys.has(issue.key)).map(issue => issue.rule));
|
|
1036
|
+
const byRule = new Map();
|
|
1037
|
+
for (const issue of eligible) {
|
|
1038
|
+
if (learnedRules.has(issue.rule))
|
|
1039
|
+
continue;
|
|
1040
|
+
const candidate = byRule.get(issue.rule) ?? { rule: issue.rule, count: 0, example: issue, keys: new Set() };
|
|
1041
|
+
if (!candidate.keys.has(issue.key)) {
|
|
1042
|
+
candidate.keys.add(issue.key);
|
|
1043
|
+
candidate.count++;
|
|
1044
|
+
}
|
|
1045
|
+
byRule.set(issue.rule, candidate);
|
|
1046
|
+
}
|
|
1047
|
+
return [...byRule.values()].map(({ rule, count, example }) => ({ rule, count, example }));
|
|
1048
|
+
}
|
|
1049
|
+
async function writeSonarReport(fs, state, audit, cwd, mode) {
|
|
1050
|
+
if (!state.sonar_policy)
|
|
1051
|
+
throw new Error('SonarQube is not enabled for this task');
|
|
1052
|
+
const path = sonarReportPath(state.id, audit);
|
|
1053
|
+
await writeText(fs, path, renderSonarReport(state.id, state.sonar_policy, audit), cwd, mode, { kind: 'createIfAbsent' });
|
|
1054
|
+
audit.report_path = path;
|
|
1055
|
+
}
|
|
1056
|
+
async function updateSonarReport(fs, state, audit, cwd, mode) {
|
|
1057
|
+
if (!state.sonar_policy || !audit.report_path)
|
|
1058
|
+
throw new Error('the current Sonar audit has no report to update');
|
|
1059
|
+
const info = await fs.lstat(audit.report_path, cwd === undefined ? undefined : { cwd });
|
|
1060
|
+
if (!info)
|
|
1061
|
+
throw new Error('the current Sonar report is missing; rerun sonar_check');
|
|
1062
|
+
await writeText(fs, audit.report_path, renderSonarReport(state.id, state.sonar_policy, audit), cwd, mode, { kind: 'replaceIfVersion', version: info.version });
|
|
1063
|
+
}
|
|
1064
|
+
async function inspectCompletedSonar(policy, token, ceTaskId, target, scopeHash, signal) {
|
|
1065
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
1066
|
+
if (signal.aborted)
|
|
1067
|
+
throw new Error('SonarQube audit was cancelled');
|
|
1068
|
+
try {
|
|
1069
|
+
return await inspectSonar(policy, token, ceTaskId, target, scopeHash, fetch, true);
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
if (!/Compute Engine task is (PENDING|IN_PROGRESS)/u.test(String(error)) || attempt === 59)
|
|
1073
|
+
throw error;
|
|
1074
|
+
await new Promise((resolve, reject) => {
|
|
1075
|
+
const timer = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve(); }, 5000);
|
|
1076
|
+
const onAbort = () => { clearTimeout(timer); reject(new Error('SonarQube audit was cancelled')); };
|
|
1077
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
throw new Error('SonarQube analysis did not finish within five minutes');
|
|
1082
|
+
}
|
|
912
1083
|
/** Resolve one item by id for the item-scoped audit operations. */
|
|
913
1084
|
function requireItem(state, id) {
|
|
914
1085
|
if (id === undefined || id === '')
|
|
@@ -954,9 +1125,9 @@ export async function approveAdvance(ctx, state, target, workflow, confirmations
|
|
|
954
1125
|
state.solution_confirmed = true;
|
|
955
1126
|
return assertAdvance(state, target, workflow);
|
|
956
1127
|
}
|
|
957
|
-
const OPERATIONS = ['status', 'assess', 'create', 'load_skill', 'record', 'items', 'scope', 'dispatch', 'review_item', 'advance', 'verify', 'sonar_check', 'learn_rule', 'review', 'commit', 'complete', 'revise', 'config', 'install_hook', 'verify_hook', 'init', 'init_project', 'set_risk', 'skill_result'];
|
|
1128
|
+
const OPERATIONS = ['status', 'assess', 'create', 'load_skill', 'record', 'items', 'scope', 'dispatch', 'review_item', 'advance', 'verify', 'sonar_check', 'sonar_disposition', 'learn_rule', 'review', 'commit', 'complete', 'revise', 'config', 'install_hook', 'verify_hook', 'init', 'init_project', 'set_risk', 'skill_result'];
|
|
958
1129
|
const TOOL_DESCRIPTION = 'Own the engineering delivery workflow as hard state. Read or create the task record, record a ' +
|
|
959
|
-
'stage artifact, record each implementation item\'s
|
|
1130
|
+
'stage artifact, record each implementation item\'s start and two-stage (spec + quality) ' +
|
|
960
1131
|
'review audit (the flow decides how many verdicts per item), advance one stage (rejected unless every configured guard already holds, including ' +
|
|
961
1132
|
'the stage artifacts; a requirement/solution confirmation guard asks a human to approve instead of ' +
|
|
962
1133
|
'being satisfied by the model), record verification or review, and gate a commit on ' +
|
|
@@ -985,9 +1156,10 @@ export function registerDevTask(ctx) {
|
|
|
985
1156
|
work_size: { type: 'string', enum: ['tiny', 'standard', 'complex'], description: 'Workload tier (create).' },
|
|
986
1157
|
complexity: { type: 'string', enum: ['low', 'medium', 'high', 'ultra'], description: 'Task-level complexity (assess/create). Select from requirement scope and implementation dependencies; independent of risk_level. Passing this on create selects the adaptive meta-skill flow.' },
|
|
987
1158
|
complexity_reason: { type: 'string', description: 'Evidence-based reason for the selected complexity (create).' },
|
|
988
|
-
ce_task_id: { type: 'string', description: 'SonarQube Compute Engine task id from
|
|
1159
|
+
ce_task_id: { type: 'string', description: 'SonarQube Compute Engine task id from a CI scan (sonar_check in CI mode). Omit for local mode: the plugin runs the scanner and reads its task id.' },
|
|
989
1160
|
pull_request: { type: 'string', description: 'GitLab merge request IID or PR number when SonarQube mode is pull-request (sonar_check).' },
|
|
990
|
-
issue_key: { type: 'string', description: 'A
|
|
1161
|
+
issue_key: { type: 'string', description: 'A SonarQube finding key: current local audit for sonar_disposition, or audit history for learn_rule.' },
|
|
1162
|
+
disposition_reason: { type: 'string', description: 'Specific explanation of why the current local finding is a false positive (sonar_disposition). Requires human approval and supporting evidence.' },
|
|
991
1163
|
rule_name: { type: 'string', description: 'New project Rule name (learn_rule).' },
|
|
992
1164
|
learning_reason: { type: 'string', description: 'Why this failure generalizes beyond the current line of code (learn_rule).' },
|
|
993
1165
|
resources: { type: 'array', description: 'Project Skill/Rule drafts for init_project propose/apply; content is reviewed before creation.',
|
|
@@ -999,7 +1171,7 @@ export function registerDevTask(ctx) {
|
|
|
999
1171
|
risk_level: { type: 'string', enum: ['standard', 'high_risk'], description: 'Risk tier (create).' },
|
|
1000
1172
|
items: {
|
|
1001
1173
|
type: 'array',
|
|
1002
|
-
description: '
|
|
1174
|
+
description: 'Implementation items (create/items). items defaults to merge by id, retaining omitted entries. Use items_mode=replace only for deliberate replanning; completed or audited items are still retained. For an existing id, omit title to preserve its title and audit.',
|
|
1003
1175
|
items: {
|
|
1004
1176
|
type: 'object',
|
|
1005
1177
|
additionalProperties: false,
|
|
@@ -1010,8 +1182,9 @@ export function registerDevTask(ctx) {
|
|
|
1010
1182
|
},
|
|
1011
1183
|
},
|
|
1012
1184
|
},
|
|
1013
|
-
|
|
1014
|
-
|
|
1185
|
+
items_mode: { type: 'string', enum: ['merge', 'replace'], description: 'items operation: merge by id (default) or explicitly replace untouched items and reorder the supplied list.' },
|
|
1186
|
+
item_id: { type: 'string', description: 'Item id to start or review (dispatch/review_item).' },
|
|
1187
|
+
description: { type: 'string', description: 'Implementation intent (dispatch); the current agent may execute it directly.' },
|
|
1015
1188
|
spec_outcome: { type: 'string', enum: ['pass', 'fail'], description: 'Specification-conformance verdict (review_item).' },
|
|
1016
1189
|
quality_outcome: { type: 'string', enum: ['pass', 'fail'], description: 'Code-quality verdict (review_item).' },
|
|
1017
1190
|
notes: { type: 'array', items: { type: 'string' }, description: 'Findings or defects (review_item).' },
|
|
@@ -1039,7 +1212,7 @@ export function registerDevTask(ctx) {
|
|
|
1039
1212
|
overwrite: { type: 'boolean', description: 'Allow replacing an existing AGENTS.md (init apply); triggers human approval.' },
|
|
1040
1213
|
expected_hash: { type: 'string', description: 'The content hash returned by init phase=propose; apply is rejected without it, or with a different hash (init apply).' },
|
|
1041
1214
|
existing_hash: { type: 'string', description: 'The existing-file hash returned by init phase=propose; when the current AGENTS.md exists, a mismatching existing_hash proves it changed during review (init apply).' },
|
|
1042
|
-
sandbox_permissions: { type: 'string', enum: ['workspace-write', 'danger-full-access'], description: 'Sandbox mode for this call\'s file writes and verify/skill_result command. Commands otherwise use the session policy. Retry
|
|
1215
|
+
sandbox_permissions: { type: 'string', enum: ['workspace-write', 'danger-full-access'], description: 'Sandbox mode for this call\'s file writes and verify/skill_result/local Sonar scanner command. Commands otherwise use the session policy. Retry a denied command with justification; danger-full-access requires approval before execution and applies only to this call.' },
|
|
1043
1216
|
justification: { type: 'string', description: 'Required with sandbox_permissions: one sentence for the user explaining why this exact operation needs the wider access.' },
|
|
1044
1217
|
phase: { type: 'string', enum: ['inspect', 'propose', 'apply'], description: 'init phase: inspect (read-only), propose (preview draft, no write), apply (write; overwriting an existing file requires human approval).' },
|
|
1045
1218
|
},
|
|
@@ -1119,6 +1292,13 @@ export function registerDevTask(ctx) {
|
|
|
1119
1292
|
if (phase !== 'propose' && phase !== 'apply')
|
|
1120
1293
|
throw new Error('init_project phase must be inspect, propose, or apply');
|
|
1121
1294
|
const resources = validateInitResources(a.resources);
|
|
1295
|
+
for (const resource of resources) {
|
|
1296
|
+
if (resource.kind !== 'skill' || resource.name !== `${inventory.project_name}-project-map`)
|
|
1297
|
+
continue;
|
|
1298
|
+
const gaps = projectMapCoverageGaps(resource.content, inventory);
|
|
1299
|
+
if (gaps.length)
|
|
1300
|
+
throw new Error(`project map omits repository-wide evidence: ${gaps.join(', ')}. Describe each discovered module or manifest in the reusable project map; keep feature-specific findings in task artifacts.`);
|
|
1301
|
+
}
|
|
1122
1302
|
const metaRaw = await readText(fs, '.dsh/meta.json', cwd);
|
|
1123
1303
|
const meta = await readAdaptiveConfig(fs, cwd);
|
|
1124
1304
|
const createdRules = new Set(resources.filter(resource => resource.kind === 'rule').map(resource => resource.name));
|
|
@@ -1142,11 +1322,13 @@ export function registerDevTask(ctx) {
|
|
|
1142
1322
|
}
|
|
1143
1323
|
}
|
|
1144
1324
|
const metaHash = hashText(metaRaw ?? '');
|
|
1145
|
-
const
|
|
1325
|
+
const inventoryHash = hashText(JSON.stringify(inventory));
|
|
1326
|
+
const proposalHash = hashText(JSON.stringify(resources) + '\n' + metaHash + '\n' + inventoryHash);
|
|
1146
1327
|
if (phase === 'propose')
|
|
1147
1328
|
return JSON.stringify({ inventory, resources, files_to_create: paths,
|
|
1329
|
+
project_map_coverage: projectMapCoveragePaths(inventory),
|
|
1148
1330
|
meta_config: '.dsh/meta.json', existing_hash: metaHash, expected_hash: proposalHash,
|
|
1149
|
-
instruction: 'Review each project-specific claim against the listed evidence. Apply with identical resources and expected_hash; existing files are protected.' }, null, 2);
|
|
1331
|
+
instruction: 'Review each project-specific claim against the listed evidence. A *-project-map Skill must describe the whole repository and remain useful across unrelated requirements; move current-feature details to task artifacts or a separately named feature Skill. Apply with identical resources and expected_hash; existing files are protected.' }, null, 2);
|
|
1150
1332
|
if (a.expected_hash !== proposalHash || a.existing_hash !== metaHash) {
|
|
1151
1333
|
throw new Error('init_project draft or meta config changed since propose; inspect and propose again');
|
|
1152
1334
|
}
|
|
@@ -1171,7 +1353,9 @@ export function registerDevTask(ctx) {
|
|
|
1171
1353
|
}
|
|
1172
1354
|
const info = await fs.lstat('.dsh/meta.json', { cwd });
|
|
1173
1355
|
await writeText(fs, '.dsh/meta.json', JSON.stringify({ ...meta, meta_bindings: bindings }, null, 2) + '\n', cwd, writeMode, info === undefined ? { kind: 'createIfAbsent' } : { kind: 'replaceIfVersion', version: info.version });
|
|
1174
|
-
return JSON.stringify({ created: paths,
|
|
1356
|
+
return JSON.stringify({ created: [...paths, '.dsh/meta.json'], bindings,
|
|
1357
|
+
team_files: [...paths, '.dsh/meta.json'],
|
|
1358
|
+
next_action: 'Review and commit the generated .dsh/skills, .dsh/rules and .dsh/meta.json as team configuration. Call assess to inspect the effective project Skills before creating a task.' }, null, 2);
|
|
1175
1359
|
}
|
|
1176
1360
|
if (a.operation === 'init') {
|
|
1177
1361
|
const phase = a.phase ?? 'inspect';
|
|
@@ -1270,6 +1454,10 @@ export function registerDevTask(ctx) {
|
|
|
1270
1454
|
throw new Error(`high_risk task cannot run on flow "${resolved.flow}" — it lacks the required capabilities ` +
|
|
1271
1455
|
`(${HIGH_RISK_REQUIRED_CAPABILITIES.join(', ')}). Use the standard flow or lower the task risk.`);
|
|
1272
1456
|
}
|
|
1457
|
+
const sonarPolicy = adaptiveConfig === undefined ? undefined : sonarPolicyFor(adaptiveConfig);
|
|
1458
|
+
if (sonarPolicy?.source === 'ide-local' && resolved.config.stages.includes('代码审核')) {
|
|
1459
|
+
resolved.config.commit = { ...resolved.config.commit, checkpoints: ['代码审核'] };
|
|
1460
|
+
}
|
|
1273
1461
|
// Freeze the resolved skill and rule bodies now. A name alone cannot keep an
|
|
1274
1462
|
// in-flight task stable: editing a rule the task uses would otherwise change
|
|
1275
1463
|
// what the task is doing without the task saying so.
|
|
@@ -1285,7 +1473,6 @@ export function registerDevTask(ctx) {
|
|
|
1285
1473
|
resources: frozen.resources,
|
|
1286
1474
|
};
|
|
1287
1475
|
const root = await detectRoot(projectProbe(fs), cwd ?? '');
|
|
1288
|
-
const sonarPolicy = adaptiveConfig === undefined ? undefined : sonarPolicyFor(adaptiveConfig);
|
|
1289
1476
|
const state = newTask({
|
|
1290
1477
|
id: a.task_id,
|
|
1291
1478
|
title: a.title,
|
|
@@ -1351,6 +1538,7 @@ export function registerDevTask(ctx) {
|
|
|
1351
1538
|
...skillResourceBlockers(missingSkillFiles, state.stage),
|
|
1352
1539
|
...resourceBlockers(missingRules, state.stage), ...staleEvidence, ...await sonarBlockers(fs, state, cwd)]
|
|
1353
1540
|
: [];
|
|
1541
|
+
const planItemBlockers = taskPlanItemBlockers(state);
|
|
1354
1542
|
return JSON.stringify({
|
|
1355
1543
|
id: state.id,
|
|
1356
1544
|
stage: state.stage,
|
|
@@ -1365,6 +1553,7 @@ export function registerDevTask(ctx) {
|
|
|
1365
1553
|
solution_confirmed: state.solution_confirmed,
|
|
1366
1554
|
items_done: `${state.items.filter(i => i.status === 'done').length}/${state.items.length}`,
|
|
1367
1555
|
items: state.items,
|
|
1556
|
+
task_plan_item_blockers: planItemBlockers,
|
|
1368
1557
|
skill_obligations: obligationStages(state, workflow).map(stage => {
|
|
1369
1558
|
const bindings = workflow.stage_bindings?.[stage]?.skills ?? [];
|
|
1370
1559
|
const skills = bindings.map(entry => state.complexity !== undefined || entry.skill.source === 'codex-project'
|
|
@@ -1402,10 +1591,12 @@ export function registerDevTask(ctx) {
|
|
|
1402
1591
|
review: state.review,
|
|
1403
1592
|
sonar: state.sonar_policy === undefined ? { enabled: false } : {
|
|
1404
1593
|
enabled: true, mode: state.sonar_policy.mode, project_key: state.sonar_policy.project_key,
|
|
1594
|
+
source: state.sonar_policy.source ?? 'ci', reference_branch: state.sonar_policy.reference_branch,
|
|
1405
1595
|
audit: state.sonar_audit ?? null, blockers: await sonarBlockers(fs, state, cwd),
|
|
1406
1596
|
prior_audits: state.sonar_history?.length ?? 0,
|
|
1407
1597
|
},
|
|
1408
1598
|
learned_rules: state.learned_rules ?? [],
|
|
1599
|
+
rule_learning_candidates: ruleLearningCandidates(state),
|
|
1409
1600
|
artifact_requirements: workflow.artifacts.filter(def => def.stage === state.stage).map(def => ({
|
|
1410
1601
|
id: def.id, name: def.name, fields: def.fields,
|
|
1411
1602
|
missing_fields: def.fields.filter(field => !(state.artifacts[def.id]?.[field] ?? '').trim()),
|
|
@@ -1436,29 +1627,131 @@ export function registerDevTask(ctx) {
|
|
|
1436
1627
|
if (!state.verification.passed || (await evidenceBlockers(fs, state, workflow, cwd)).length) {
|
|
1437
1628
|
throw new Error('functional verification must pass on the current files before sonar_check');
|
|
1438
1629
|
}
|
|
1439
|
-
if (!state.commits.some(commit => commit.hash !== undefined)) {
|
|
1440
|
-
throw new Error('sonar_check requires a recorded Git commit
|
|
1630
|
+
if (state.sonar_policy.source !== 'ide-local' && !state.commits.some(commit => commit.hash !== undefined)) {
|
|
1631
|
+
throw new Error('sonar_check requires a recorded Git commit at the 测试 checkpoint; CI mode also requires a push');
|
|
1441
1632
|
}
|
|
1442
|
-
if (!a.ce_task_id)
|
|
1443
|
-
throw new Error('sonar_check requires ce_task_id from this task\'s completed CI scan');
|
|
1444
1633
|
const target = state.sonar_policy.mode === 'branch' ? state.branch : a.pull_request;
|
|
1445
1634
|
if (!target)
|
|
1446
1635
|
throw new Error('pull-request SonarQube audit requires pull_request');
|
|
1447
|
-
const
|
|
1636
|
+
const credentialProvider = ctx.get('credentials');
|
|
1637
|
+
const token = await resolveSonarToken(credentialProvider, state.root, process.env[state.sonar_policy.token_env]);
|
|
1638
|
+
const scopeBefore = await scopeFingerprint(fs, state, cwd);
|
|
1639
|
+
if (state.sonar_policy.source === 'ide-local') {
|
|
1640
|
+
if (a.ce_task_id)
|
|
1641
|
+
throw new Error('本地规则审核不使用 CI ce_task_id');
|
|
1642
|
+
if (!state.files.length)
|
|
1643
|
+
throw new Error('本地规则审核需要先登记当前任务的文件范围');
|
|
1644
|
+
const root = state.root ?? cwd;
|
|
1645
|
+
if (!root)
|
|
1646
|
+
throw new Error('本地规则审核需要项目工作区');
|
|
1647
|
+
const audit = await inspectLocalRules(state.sonar_policy, token, root, scopeBefore, exec.signal, state.files);
|
|
1648
|
+
if (await scopeFingerprint(fs, state, cwd) !== scopeBefore) {
|
|
1649
|
+
throw new Error('本地规则审核期间代码发生变化,请重新测试和审核');
|
|
1650
|
+
}
|
|
1651
|
+
if (state.sonar_audit !== undefined)
|
|
1652
|
+
state.sonar_history = [...(state.sonar_history ?? []), state.sonar_audit].slice(-20);
|
|
1653
|
+
state.sonar_audit = audit;
|
|
1654
|
+
state.review.outcome = 'pending';
|
|
1655
|
+
const mode = await resolveWriteMode(ctx, a, exec);
|
|
1656
|
+
await writeSonarReport(fs, state, audit, cwd, mode);
|
|
1657
|
+
await writeTask(fs, state, cwd, mode);
|
|
1658
|
+
return JSON.stringify({ gate: audit.gate, blocking: audit.blocking, findings: audit.findings,
|
|
1659
|
+
uncovered_files: audit.uncovered_files ?? [], report_path: audit.report_path,
|
|
1660
|
+
rule_learning_candidates: ruleLearningCandidates(state),
|
|
1661
|
+
next_action: audit.gate === 'OK' ? '继续代码审核;检查历史阻断问题是否有可复用的修复经验,再决定是否通过 learn_rule 提议项目 Rule。误报或一次性问题不要沉淀。'
|
|
1662
|
+
: '修复新增代码问题,并处理未覆盖文件;重新测试后再次运行 sonar_check。' }, null, 2);
|
|
1663
|
+
}
|
|
1664
|
+
let ceTaskId = a.ce_task_id;
|
|
1665
|
+
if (state.sonar_policy.source === 'local') {
|
|
1666
|
+
if (ceTaskId)
|
|
1667
|
+
throw new Error('local sonar_check runs a fresh scan; do not provide a CI ce_task_id');
|
|
1668
|
+
const root = state.root ?? cwd;
|
|
1669
|
+
if (!root)
|
|
1670
|
+
throw new Error('local SonarQube scan needs a project workspace');
|
|
1671
|
+
const head = await runVerificationCommand(ctx, 'git rev-parse HEAD', root, exec);
|
|
1672
|
+
const branch = await runVerificationCommand(ctx, 'git branch --show-current', root, exec);
|
|
1673
|
+
const status = await runVerificationCommand(ctx, 'git status --porcelain --untracked-files=all', root, exec);
|
|
1674
|
+
const lastCommit = state.commits.findLast(entry => entry.hash !== undefined)?.hash;
|
|
1675
|
+
if (head.exit_code !== 0 || branch.exit_code !== 0 || status.exit_code !== 0 || !lastCommit
|
|
1676
|
+
|| !head.stdout.trim().startsWith(lastCommit) || branch.stdout.trim() !== state.branch) {
|
|
1677
|
+
throw new Error('local SonarQube scan requires the recorded commit checked out on the task branch');
|
|
1678
|
+
}
|
|
1679
|
+
const dirty = status.stdout.split(/\r?\n/u).filter(line => line.trim() && !/^..\s+\.dsh[\\/]/u.test(line));
|
|
1680
|
+
if (dirty.length)
|
|
1681
|
+
throw new Error('local SonarQube scan requires a clean working tree outside .dsh; commit or remove unrecorded changes first');
|
|
1682
|
+
await assertLocalSonarReady(state.sonar_policy, token);
|
|
1683
|
+
const defaultCommand = await readText(fs, 'pom.xml', root) === undefined ? 'sonar-scanner'
|
|
1684
|
+
: 'mvn org.sonarsource.scanner.maven:sonar-maven-plugin:5.5.0.6356:sonar';
|
|
1685
|
+
const approvedMode = a.sandbox_permissions === undefined ? undefined : await resolveWriteMode(ctx, { ...a, command: state.sonar_policy.scan_command?.trim() || defaultCommand }, exec);
|
|
1686
|
+
ceTaskId = await runLocalSonar(ctx, state.sonar_policy, token, root, state.branch, exec, defaultCommand, approvedMode);
|
|
1687
|
+
}
|
|
1688
|
+
if (!ceTaskId)
|
|
1689
|
+
throw new Error('sonar_check requires ce_task_id from this task\'s completed CI scan');
|
|
1690
|
+
if (state.sonar_policy.source === 'local' && await scopeFingerprint(fs, state, cwd) !== scopeBefore) {
|
|
1691
|
+
throw new Error('local SonarQube scan changed task files; rerun functional verification before auditing');
|
|
1692
|
+
}
|
|
1693
|
+
const audit = await inspectCompletedSonar(state.sonar_policy, token, ceTaskId, target, scopeBefore, exec.signal);
|
|
1448
1694
|
audit.commit_hash = state.commits.findLast(entry => entry.hash !== undefined).hash;
|
|
1449
1695
|
if (state.sonar_audit !== undefined)
|
|
1450
1696
|
state.sonar_history = [...(state.sonar_history ?? []), state.sonar_audit].slice(-20);
|
|
1451
1697
|
state.sonar_audit = audit;
|
|
1452
1698
|
state.review.outcome = 'pending';
|
|
1453
|
-
|
|
1454
|
-
|
|
1699
|
+
const mode = await resolveWriteMode(ctx, a, exec);
|
|
1700
|
+
await writeSonarReport(fs, state, audit, cwd, mode);
|
|
1701
|
+
await writeTask(fs, state, cwd, mode);
|
|
1702
|
+
return JSON.stringify({ gate: audit.gate, ...(audit.server_gate ? { server_gate: audit.server_gate } : {}),
|
|
1703
|
+
blocking: audit.blocking, findings: audit.findings, report_path: audit.report_path,
|
|
1704
|
+
rule_learning_candidates: ruleLearningCandidates(state),
|
|
1455
1705
|
analysis_id: audit.analysis_id, next_action: audit.gate === 'OK' && audit.blocking.length === 0
|
|
1456
|
-
? 'Continue code review, then record review pass.'
|
|
1457
|
-
: 'Record review blocked, fix findings, rerun tests and
|
|
1706
|
+
? 'Review prior blocking issues for reusable fixes with learn_rule; do not learn false positives. Continue code review, then record review pass.'
|
|
1707
|
+
: 'Record review blocked, fix findings, rerun tests and the scan, then call sonar_check again.' }, null, 2);
|
|
1708
|
+
}
|
|
1709
|
+
if (a.operation === 'sonar_disposition') {
|
|
1710
|
+
if (state.stage !== '代码审核' || state.sonar_policy?.source !== 'ide-local' || !state.sonar_audit) {
|
|
1711
|
+
throw new Error('sonar_disposition only applies to the current ide-local audit in the 代码审核 stage');
|
|
1712
|
+
}
|
|
1713
|
+
if (state.sonar_audit.scope_hash !== await scopeFingerprint(fs, state, cwd)) {
|
|
1714
|
+
throw new Error('the local audit is stale after code changes; rerun verification and sonar_check');
|
|
1715
|
+
}
|
|
1716
|
+
const issue = state.sonar_audit.blocking.find(entry => entry.key === a.issue_key);
|
|
1717
|
+
if (!issue)
|
|
1718
|
+
throw new Error('issue_key must identify a blocking finding in the current local audit');
|
|
1719
|
+
if (state.sonar_audit.dispositions?.some(entry => entry.issue_key === issue.key)) {
|
|
1720
|
+
throw new Error('this finding already has an approved disposition');
|
|
1721
|
+
}
|
|
1722
|
+
if (!a.disposition_reason?.trim() || a.disposition_reason.trim().length < 30
|
|
1723
|
+
|| a.disposition_reason.length > 2000 || !a.evidence?.length || a.evidence.length > 6
|
|
1724
|
+
|| a.evidence.some(item => !item.trim() || item.length > 500)) {
|
|
1725
|
+
throw new Error('false-positive disposition needs a specific reason of at least 30 characters and non-empty supporting evidence');
|
|
1726
|
+
}
|
|
1727
|
+
const approval = ctx.get('approval');
|
|
1728
|
+
if (!approval || !exec.agent)
|
|
1729
|
+
throw new Error('false-positive disposition requires the human approval service');
|
|
1730
|
+
const outcome = await approval.request({ agent: exec.agent, toolName: 'dev_task', callId: exec.callId,
|
|
1731
|
+
reason: `本地 Sonar 误报复核:${issue.rule} · ${issue.file}${issue.line === undefined ? '' : `:${issue.line}`}(${issue.key})。\n原因:${a.disposition_reason.trim()}\n证据:${a.evidence.join(';')}\n仅对此次未变化的本地审核结果有效,不修改服务端规则或原始结果。是否批准?`,
|
|
1732
|
+
signal: exec.signal });
|
|
1733
|
+
if (outcome !== 'allowed-once')
|
|
1734
|
+
throw new Error(`false-positive disposition was not approved (${outcome})`);
|
|
1735
|
+
if (state.sonar_audit.scope_hash !== await scopeFingerprint(fs, state, cwd)) {
|
|
1736
|
+
throw new Error('code changed during false-positive approval; rerun verification and sonar_check');
|
|
1737
|
+
}
|
|
1738
|
+
state.sonar_audit.dispositions = [...(state.sonar_audit.dispositions ?? []), {
|
|
1739
|
+
issue_key: issue.key, kind: 'false_positive', reason: a.disposition_reason.trim(),
|
|
1740
|
+
evidence: a.evidence, approved_at: new Date().toISOString(),
|
|
1741
|
+
}];
|
|
1742
|
+
state.review.outcome = 'pending';
|
|
1743
|
+
const mode = await resolveWriteMode(ctx, a, exec);
|
|
1744
|
+
await updateSonarReport(fs, state, state.sonar_audit, cwd, mode);
|
|
1745
|
+
await writeTask(fs, state, cwd, mode);
|
|
1746
|
+
return JSON.stringify({ approved_issue: issue.key, raw_gate: state.sonar_audit.gate,
|
|
1747
|
+
review_gate: localReviewGate(state.sonar_audit),
|
|
1748
|
+
unresolved_blocking: unresolvedBlockingFindings(state.sonar_audit).length,
|
|
1749
|
+
report_path: state.sonar_audit.report_path,
|
|
1750
|
+
next_action: '其余问题仍需修复或逐项复核;代码或规则变化后重新测试并运行 sonar_check。' }, null, 2);
|
|
1458
1751
|
}
|
|
1459
1752
|
if (a.operation === 'learn_rule') {
|
|
1460
|
-
const issue = [state.sonar_audit, ...(state.sonar_history ?? [])].filter(entry => entry !== undefined)
|
|
1461
|
-
.flatMap(entry => entry
|
|
1753
|
+
const issue = [state.sonar_audit, ...(state.sonar_history ?? [])].filter((entry) => entry !== undefined)
|
|
1754
|
+
.flatMap(entry => unresolvedBlockingFindings(entry)).find(entry => entry.key === a.issue_key);
|
|
1462
1755
|
if (!issue)
|
|
1463
1756
|
throw new Error('learn_rule requires a blocking issue_key recorded by this task\'s SonarQube audit');
|
|
1464
1757
|
if (!a.rule_name || !RESOURCE_NAME.test(a.rule_name) || !a.content?.trim() || a.content.trim().length < 40
|
|
@@ -1655,7 +1948,7 @@ export function registerDevTask(ctx) {
|
|
|
1655
1948
|
note = `file scope set to ${state.files.length} files${state.files.length > 0 ? ': ' + state.files.join(', ') : ' (empty — commits are blocked until files are declared)'}`;
|
|
1656
1949
|
break;
|
|
1657
1950
|
case 'items':
|
|
1658
|
-
state.items = normalizeItems(a.items, state.items);
|
|
1951
|
+
state.items = normalizeItems(a.items, state.items, a.items_mode ?? 'merge');
|
|
1659
1952
|
note = `items set: ${state.items.map(i => `${i.id}:${i.status}`).join(', ') || 'none'}`;
|
|
1660
1953
|
break;
|
|
1661
1954
|
case 'dispatch': {
|
|
@@ -1665,7 +1958,7 @@ export function registerDevTask(ctx) {
|
|
|
1665
1958
|
item.status = 'doing';
|
|
1666
1959
|
delete item.review;
|
|
1667
1960
|
item.dispatch = { description: a.description ?? '', at: new Date().toISOString() };
|
|
1668
|
-
note = `item "${item.id}" is doing;
|
|
1961
|
+
note = `item "${item.id}" is doing; implementation intent recorded: ${item.dispatch.description || '(no description)'}. Implement it directly or with help, then review_item and mark it done.`;
|
|
1669
1962
|
break;
|
|
1670
1963
|
}
|
|
1671
1964
|
case 'review_item': {
|
|
@@ -1684,6 +1977,9 @@ export function registerDevTask(ctx) {
|
|
|
1684
1977
|
}
|
|
1685
1978
|
case 'advance': {
|
|
1686
1979
|
const target = a.target_stage ?? '';
|
|
1980
|
+
const planItemBlockers = taskPlanItemBlockers(state);
|
|
1981
|
+
if (planItemBlockers.length)
|
|
1982
|
+
throw new Error(planItemBlockers.join('; '));
|
|
1687
1983
|
await assertFreshEvidence(fs, state, workflow, cwd);
|
|
1688
1984
|
const missingSkills = skillBlockers(state, workflow, session);
|
|
1689
1985
|
if (missingSkills.length)
|
|
@@ -1777,8 +2073,12 @@ export function registerDevTask(ctx) {
|
|
|
1777
2073
|
if (receipt.root !== undefined && receipt.root !== verifyRoot) {
|
|
1778
2074
|
throw new Error(`verification receipt root mismatch: ${receipt.root} vs task root ${String(verifyRoot)}`);
|
|
1779
2075
|
}
|
|
2076
|
+
const testSummary = mavenTestEvidence(command, `${receipt.stdout}\n${receipt.stderr}`);
|
|
2077
|
+
if (testSummary)
|
|
2078
|
+
receipt.test_summary = testSummary;
|
|
1780
2079
|
state.verification = {
|
|
1781
|
-
passed: receipt.exit_code === 0 && !receipt.timed_out && !receipt.aborted && !receipt.sandbox?.denied
|
|
2080
|
+
passed: receipt.exit_code === 0 && !receipt.timed_out && !receipt.aborted && !receipt.sandbox?.denied
|
|
2081
|
+
&& !receipt.sandbox?.runnerFailed && (testSummary === null || (testSummary.count ?? 0) > 0),
|
|
1782
2082
|
evidence: a.evidence ?? [],
|
|
1783
2083
|
receipt,
|
|
1784
2084
|
};
|