@godv61/dsh-task-engine 0.29.3 → 0.29.8
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 +302 -149
- package/.codex-project-test.mjs +18 -18
- package/.evidence-test.mjs +16 -3
- package/.hook-test.mjs +19 -19
- package/.resource-test.mjs +19 -2
- package/.roundtrip-test.mjs +8 -3
- package/.sonar-credential-test.mjs +38 -0
- package/.sonarlint-local-test.mjs +65 -0
- package/.workflow-test.mjs +29 -12
- package/README.md +22 -22
- package/docs/CHANGELOG.md +33 -19
- package/docs/README.md +6 -6
- package/docs/adaptive-workflows.md +85 -73
- package/docs/configuration.md +4 -4
- package/docs/faq.md +24 -18
- package/docs/getting-started.md +9 -9
- package/docs/manual-legacy.html +1 -1
- package/docs/manual.html +124 -117
- package/docs/roadmap.md +7 -7
- package/hooks/commit-msg +1 -1
- package/lib/adaptive.js +1 -1
- package/lib/adaptive.js.map +1 -1
- package/lib/client.js +289 -17
- package/lib/client.js.map +2 -2
- package/lib/controller.d.ts +35 -0
- package/lib/controller.js +68 -0
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.d.ts +2 -0
- package/lib/dev-task.js +261 -31
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +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 +40 -0
- package/lib/sonar-report.js.map +1 -0
- package/lib/sonar.d.ts +23 -2
- package/lib/sonar.js +55 -4
- package/lib/sonar.js.map +1 -1
- package/lib/sonarlint-local.d.ts +15 -0
- package/lib/sonarlint-local.js +307 -0
- package/lib/sonarlint-local.js.map +1 -0
- package/package.json +9 -7
- 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 +7 -7
- package/skills/architecture-design/SKILL.md +11 -11
- package/skills/code-development/SKILL.md +11 -11
- package/skills/code-review/SKILL.md +11 -11
- package/skills/eng-delivery/SKILL.md +6 -4
- package/skills/requirements-analysis/SKILL.md +11 -11
- package/skills/task-orchestration/SKILL.md +11 -11
- package/skills/test-validation/SKILL.md +11 -11
package/lib/dev-task.js
CHANGED
|
@@ -10,16 +10,19 @@
|
|
|
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, 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";
|
|
25
28
|
import { loadedSkills, needsSkillReceipt, obligationStages, skillBlockers } from "./skill-audit.js";
|
|
@@ -101,6 +104,13 @@ async function resolveVerifyCommand(fs, cwd) {
|
|
|
101
104
|
}
|
|
102
105
|
/** Hard cap on a verification command's run time; a timeout marks the receipt non-passing. */
|
|
103
106
|
const VERIFY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
107
|
+
async function runShell(shell, spec) {
|
|
108
|
+
if (typeof shell.execute === 'function')
|
|
109
|
+
return (await shell.execute(spec)).result();
|
|
110
|
+
if (typeof shell.run === 'function')
|
|
111
|
+
return shell.run(spec);
|
|
112
|
+
throw new Error('host shell service has neither execute nor run');
|
|
113
|
+
}
|
|
104
114
|
/**
|
|
105
115
|
* Run a verification command through the host shell service and return an
|
|
106
116
|
* objective {@link VerificationReceipt}; `passed` derives from the exit code,
|
|
@@ -122,7 +132,7 @@ async function runVerificationCommand(ctx, command, cwd, exec, approvedMode) {
|
|
|
122
132
|
timeoutMs: VERIFY_TIMEOUT_MS, signal: exec.signal,
|
|
123
133
|
...(sandboxPolicy ? { sandboxPolicy } : {}),
|
|
124
134
|
});
|
|
125
|
-
const outcome = await shell
|
|
135
|
+
const outcome = await runShell(shell, spec);
|
|
126
136
|
return {
|
|
127
137
|
command,
|
|
128
138
|
exit_code: outcome.exitCode ?? -1,
|
|
@@ -136,6 +146,55 @@ async function runVerificationCommand(ctx, command, cwd, exec, approvedMode) {
|
|
|
136
146
|
...(outcome.sandbox ? { sandbox: outcome.sandbox } : {}),
|
|
137
147
|
};
|
|
138
148
|
}
|
|
149
|
+
/** Run a local scanner without putting the credential on a command line or in the task record. */
|
|
150
|
+
async function runLocalSonar(ctx, policy, token, root, branch, exec, defaultCommand, approvedMode) {
|
|
151
|
+
if (!token)
|
|
152
|
+
throw new Error('SonarQube Token is not configured for this project');
|
|
153
|
+
if (!/^[A-Za-z0-9._/-]+$/u.test(branch) || !policy.reference_branch
|
|
154
|
+
|| branch === policy.reference_branch || !/^[A-Za-z0-9._/-]+$/u.test(policy.reference_branch)) {
|
|
155
|
+
throw new Error('local SonarQube scan needs a safe current branch and a different reference branch');
|
|
156
|
+
}
|
|
157
|
+
if (!/^[A-Za-z0-9_.:-]+$/u.test(policy.project_key))
|
|
158
|
+
throw new Error('invalid SonarQube project key');
|
|
159
|
+
const shell = ctx.get('shell');
|
|
160
|
+
if (!shell)
|
|
161
|
+
throw new Error('local SonarQube scan needs the host shell service');
|
|
162
|
+
const temporary = mkdtempSync(join(tmpdir(), 'dsh-sonar-'));
|
|
163
|
+
try {
|
|
164
|
+
const metadata = join(temporary, 'report-task.txt');
|
|
165
|
+
const quote = (value) => `'${value.replace(/'/gu, "''")}'`;
|
|
166
|
+
const command = [policy.scan_command?.trim() || defaultCommand,
|
|
167
|
+
`-Dsonar.projectKey=${quote(policy.project_key)}`,
|
|
168
|
+
`-Dsonar.branch.name=${quote(branch)}`,
|
|
169
|
+
`-Dsonar.newCode.referenceBranch=${quote(policy.reference_branch)}`,
|
|
170
|
+
`-Dsonar.scanner.metadataFilePath=${quote(metadata)}`,
|
|
171
|
+
'-Dsonar.qualitygate.wait=false'].join(' ');
|
|
172
|
+
const policyService = ctx.get('sandboxPolicy');
|
|
173
|
+
const standingPolicy = policyService?.resolve({ session: exec.agent?.session });
|
|
174
|
+
const sandboxPolicy = approvedMode === undefined ? standingPolicy : { ...standingPolicy, mode: approvedMode };
|
|
175
|
+
const spec = shell.resolve({ command, workdir: root, timeoutMs: 30 * 60 * 1000, signal: exec.signal,
|
|
176
|
+
env: { SONAR_TOKEN: token, SONAR_HOST_URL: policy.host_url },
|
|
177
|
+
...(sandboxPolicy ? { sandboxPolicy } : {}) });
|
|
178
|
+
const outcome = await runShell(shell, spec);
|
|
179
|
+
if (outcome.exitCode !== 0 || outcome.aborted || outcome.timedOut || outcome.sandbox?.denied || outcome.sandbox?.runnerFailed) {
|
|
180
|
+
throw new Error(`local SonarQube scanner did not complete (exit ${outcome.exitCode ?? 'unknown'}); inspect scanner output and retry`);
|
|
181
|
+
}
|
|
182
|
+
let report;
|
|
183
|
+
try {
|
|
184
|
+
report = readFileSync(metadata, 'utf8');
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
throw new Error('local SonarQube scanner did not write report-task.txt; check scanner metadataFilePath support');
|
|
188
|
+
}
|
|
189
|
+
const id = /^ceTaskId=(.+)$/mu.exec(report)?.[1]?.trim();
|
|
190
|
+
if (!id || !/^[\w-]{8,120}$/u.test(id))
|
|
191
|
+
throw new Error('local SonarQube scanner returned an invalid ceTaskId');
|
|
192
|
+
return id;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
139
198
|
function sanitize(id) {
|
|
140
199
|
return id.replace(/[^0-9a-zA-Z_-]/g, '-');
|
|
141
200
|
}
|
|
@@ -462,7 +521,26 @@ function sonarPolicyFor(project) {
|
|
|
462
521
|
const mode = sonar.mode ?? 'branch';
|
|
463
522
|
if (mode !== 'branch' && mode !== 'pull-request')
|
|
464
523
|
throw new Error('sonar.mode must be branch or pull-request');
|
|
465
|
-
|
|
524
|
+
const source = sonar.source ?? 'ci';
|
|
525
|
+
if (source !== 'ci' && source !== 'local' && source !== 'ide-local')
|
|
526
|
+
throw new Error('sonar.source must be ci, local, or ide-local');
|
|
527
|
+
if ((source === 'local' || source === 'ide-local') && (mode !== 'branch' || !sonar.reference_branch?.trim())) {
|
|
528
|
+
throw new Error('local Sonar review needs branch mode and a reference_branch for new code');
|
|
529
|
+
}
|
|
530
|
+
if (sonar.reference_branch && !/^[A-Za-z0-9._/-]+$/u.test(sonar.reference_branch))
|
|
531
|
+
throw new Error('invalid SonarQube reference_branch');
|
|
532
|
+
if (sonar.scan_command && !validLocalScanCommand(sonar.scan_command)) {
|
|
533
|
+
throw new Error('scan_command must be a single Maven or SonarScanner command without shell operators or Token arguments');
|
|
534
|
+
}
|
|
535
|
+
if (sonar.include_paths !== undefined && !validAuditIncludePaths(sonar.include_paths)) {
|
|
536
|
+
throw new Error('sonar.include_paths must contain safe project-relative paths');
|
|
537
|
+
}
|
|
538
|
+
if (sonar.include_paths?.length && source !== 'ide-local')
|
|
539
|
+
throw new Error('sonar.include_paths only applies to ide-local reviews');
|
|
540
|
+
return { enabled: true, host_url, project_key, token_env, mode, source,
|
|
541
|
+
...(sonar.reference_branch ? { reference_branch: sonar.reference_branch } : {}),
|
|
542
|
+
...(sonar.scan_command ? { scan_command: sonar.scan_command } : {}),
|
|
543
|
+
...(sonar.include_paths?.length ? { include_paths: sonar.include_paths } : {}) };
|
|
466
544
|
}
|
|
467
545
|
async function resolveLogicalSkill(name, fs, cwd) {
|
|
468
546
|
if (!RESOURCE_NAME.test(name))
|
|
@@ -804,9 +882,13 @@ function normalizeItems(items, previous = []) {
|
|
|
804
882
|
const unchanged = old?.title === title && !(old.status === 'done' && status !== 'done');
|
|
805
883
|
return { ...(unchanged ? old : {}), id, title, status };
|
|
806
884
|
});
|
|
807
|
-
|
|
885
|
+
// Replanning untouched work must not silently erase completed items or their audit.
|
|
886
|
+
const retained = previous.filter(item => !ids.has(item.id)
|
|
887
|
+
&& (item.status === 'done' || item.dispatch !== undefined || item.review !== undefined));
|
|
888
|
+
if ([...result, ...retained].filter(item => item.status === 'doing').length > 1) {
|
|
808
889
|
throw new Error('only one implementation item may be doing');
|
|
809
|
-
|
|
890
|
+
}
|
|
891
|
+
return [...result, ...retained];
|
|
810
892
|
}
|
|
811
893
|
/** Hash declared deliverables through host FS; task bookkeeping is excluded to avoid self-invalidation. */
|
|
812
894
|
async function scopeFingerprint(fs, state, cwd) {
|
|
@@ -899,16 +981,84 @@ async function sonarBlockers(fs, state, cwd) {
|
|
|
899
981
|
const current = await scopeFingerprint(fs, state, cwd);
|
|
900
982
|
const problems = [];
|
|
901
983
|
if (state.sonar_audit.scope_hash !== current)
|
|
902
|
-
problems.push('SonarQube audit is stale after file changes; rerun
|
|
984
|
+
problems.push('SonarQube audit is stale after file changes; rerun the scan and sonar_check');
|
|
903
985
|
const commit = state.commits.findLast(entry => entry.hash !== undefined)?.hash;
|
|
904
|
-
if (!commit || state.sonar_audit.commit_hash !== commit)
|
|
986
|
+
if (state.sonar_policy.source !== 'ide-local' && (!commit || state.sonar_audit.commit_hash !== commit)) {
|
|
905
987
|
problems.push('SonarQube audit does not match the task\'s latest recorded commit');
|
|
988
|
+
}
|
|
906
989
|
if (state.sonar_audit.gate !== 'OK')
|
|
907
|
-
problems.push(
|
|
990
|
+
problems.push(state.sonar_policy.source === 'ide-local'
|
|
991
|
+
? '本地规则审核未通过' : `SonarQube Quality Gate is ${state.sonar_audit.gate}`);
|
|
908
992
|
if (state.sonar_audit.blocking.length)
|
|
909
993
|
problems.push(`${state.sonar_audit.blocking.length} medium/high SonarQube new-code findings remain`);
|
|
994
|
+
if (state.sonar_audit.uncovered_files?.length)
|
|
995
|
+
problems.push(`本地规则审核未覆盖 ${state.sonar_audit.uncovered_files.length} 个新增代码文件`);
|
|
910
996
|
return problems;
|
|
911
997
|
}
|
|
998
|
+
/** Explicit item headings in a task plan, for checking plan/ledger consistency. */
|
|
999
|
+
export function plannedItemIds(steps) {
|
|
1000
|
+
return [...new Set(steps.split(/\r?\n/u).flatMap(line => {
|
|
1001
|
+
const match = /^\s*(?:[-*+]\s+|#{1,6}\s+)?([A-Za-z][A-Za-z0-9_-]*\d+)\b/u.exec(line);
|
|
1002
|
+
return match === null ? [] : [match[1]];
|
|
1003
|
+
}))];
|
|
1004
|
+
}
|
|
1005
|
+
function taskPlanItemBlockers(state) {
|
|
1006
|
+
if (state.stage !== '任务编排' || state.complexity === undefined)
|
|
1007
|
+
return [];
|
|
1008
|
+
const ids = plannedItemIds(state.artifacts['task-plan']?.steps ?? '');
|
|
1009
|
+
if (!ids.length)
|
|
1010
|
+
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'];
|
|
1011
|
+
const recorded = new Set(state.items.map(item => item.id));
|
|
1012
|
+
const missing = ids.filter(id => !recorded.has(id));
|
|
1013
|
+
const unplanned = state.items.filter(item => !ids.includes(item.id)).map(item => item.id);
|
|
1014
|
+
return [...(missing.length ? [`task plan items missing from ledger: ${missing.join(', ')}`] : []),
|
|
1015
|
+
...(unplanned.length ? [`ledger items absent from task plan: ${unplanned.join(', ')}`] : [])];
|
|
1016
|
+
}
|
|
1017
|
+
/** One optional, reviewable learning candidate per server rule, never an automatic Rule. */
|
|
1018
|
+
function ruleLearningCandidates(state) {
|
|
1019
|
+
const audits = [...(state.sonar_history ?? []), state.sonar_audit].filter((audit) => audit !== undefined);
|
|
1020
|
+
const learnedKeys = new Set((state.learned_rules ?? []).map(entry => entry.issue_key));
|
|
1021
|
+
const learnedRules = new Set(audits.flatMap(audit => audit.blocking)
|
|
1022
|
+
.filter(issue => learnedKeys.has(issue.key)).map(issue => issue.rule));
|
|
1023
|
+
const byRule = new Map();
|
|
1024
|
+
for (const issue of audits.flatMap(audit => audit.blocking)) {
|
|
1025
|
+
if (learnedRules.has(issue.rule))
|
|
1026
|
+
continue;
|
|
1027
|
+
const candidate = byRule.get(issue.rule) ?? { rule: issue.rule, count: 0, example: issue, keys: new Set() };
|
|
1028
|
+
if (!candidate.keys.has(issue.key)) {
|
|
1029
|
+
candidate.keys.add(issue.key);
|
|
1030
|
+
candidate.count++;
|
|
1031
|
+
}
|
|
1032
|
+
byRule.set(issue.rule, candidate);
|
|
1033
|
+
}
|
|
1034
|
+
return [...byRule.values()].map(({ rule, count, example }) => ({ rule, count, example }));
|
|
1035
|
+
}
|
|
1036
|
+
async function writeSonarReport(fs, state, audit, cwd, mode) {
|
|
1037
|
+
if (!state.sonar_policy)
|
|
1038
|
+
throw new Error('SonarQube is not enabled for this task');
|
|
1039
|
+
const path = sonarReportPath(state.id, audit);
|
|
1040
|
+
await writeText(fs, path, renderSonarReport(state.id, state.sonar_policy, audit), cwd, mode, { kind: 'createIfAbsent' });
|
|
1041
|
+
audit.report_path = path;
|
|
1042
|
+
}
|
|
1043
|
+
async function inspectCompletedSonar(policy, token, ceTaskId, target, scopeHash, signal) {
|
|
1044
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
1045
|
+
if (signal.aborted)
|
|
1046
|
+
throw new Error('SonarQube audit was cancelled');
|
|
1047
|
+
try {
|
|
1048
|
+
return await inspectSonar(policy, token, ceTaskId, target, scopeHash, fetch, true);
|
|
1049
|
+
}
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
if (!/Compute Engine task is (PENDING|IN_PROGRESS)/u.test(String(error)) || attempt === 59)
|
|
1052
|
+
throw error;
|
|
1053
|
+
await new Promise((resolve, reject) => {
|
|
1054
|
+
const timer = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve(); }, 5000);
|
|
1055
|
+
const onAbort = () => { clearTimeout(timer); reject(new Error('SonarQube audit was cancelled')); };
|
|
1056
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
throw new Error('SonarQube analysis did not finish within five minutes');
|
|
1061
|
+
}
|
|
912
1062
|
/** Resolve one item by id for the item-scoped audit operations. */
|
|
913
1063
|
function requireItem(state, id) {
|
|
914
1064
|
if (id === undefined || id === '')
|
|
@@ -956,7 +1106,7 @@ export async function approveAdvance(ctx, state, target, workflow, confirmations
|
|
|
956
1106
|
}
|
|
957
1107
|
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'];
|
|
958
1108
|
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
|
|
1109
|
+
'stage artifact, record each implementation item\'s start and two-stage (spec + quality) ' +
|
|
960
1110
|
'review audit (the flow decides how many verdicts per item), advance one stage (rejected unless every configured guard already holds, including ' +
|
|
961
1111
|
'the stage artifacts; a requirement/solution confirmation guard asks a human to approve instead of ' +
|
|
962
1112
|
'being satisfied by the model), record verification or review, and gate a commit on ' +
|
|
@@ -985,7 +1135,7 @@ export function registerDevTask(ctx) {
|
|
|
985
1135
|
work_size: { type: 'string', enum: ['tiny', 'standard', 'complex'], description: 'Workload tier (create).' },
|
|
986
1136
|
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
1137
|
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
|
|
1138
|
+
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
1139
|
pull_request: { type: 'string', description: 'GitLab merge request IID or PR number when SonarQube mode is pull-request (sonar_check).' },
|
|
990
1140
|
issue_key: { type: 'string', description: 'A blocking SonarQube issue key from this task\'s audit history (learn_rule).' },
|
|
991
1141
|
rule_name: { type: 'string', description: 'New project Rule name (learn_rule).' },
|
|
@@ -999,7 +1149,7 @@ export function registerDevTask(ctx) {
|
|
|
999
1149
|
risk_level: { type: 'string', enum: ['standard', 'high_risk'], description: 'Risk tier (create).' },
|
|
1000
1150
|
items: {
|
|
1001
1151
|
type: 'array',
|
|
1002
|
-
description: '
|
|
1152
|
+
description: 'Implementation item list (create/replace). Completed or audited items are retained even when omitted; omitted untouched items are removed. For an existing id, omit title to preserve its title and audit. Changing a reviewed title requires reopening as todo/doing and reviewing again.',
|
|
1003
1153
|
items: {
|
|
1004
1154
|
type: 'object',
|
|
1005
1155
|
additionalProperties: false,
|
|
@@ -1010,8 +1160,8 @@ export function registerDevTask(ctx) {
|
|
|
1010
1160
|
},
|
|
1011
1161
|
},
|
|
1012
1162
|
},
|
|
1013
|
-
item_id: { type: 'string', description: 'Item id to
|
|
1014
|
-
description: { type: 'string', description: '
|
|
1163
|
+
item_id: { type: 'string', description: 'Item id to start or review (dispatch/review_item).' },
|
|
1164
|
+
description: { type: 'string', description: 'Implementation intent (dispatch); the current agent may execute it directly.' },
|
|
1015
1165
|
spec_outcome: { type: 'string', enum: ['pass', 'fail'], description: 'Specification-conformance verdict (review_item).' },
|
|
1016
1166
|
quality_outcome: { type: 'string', enum: ['pass', 'fail'], description: 'Code-quality verdict (review_item).' },
|
|
1017
1167
|
notes: { type: 'array', items: { type: 'string' }, description: 'Findings or defects (review_item).' },
|
|
@@ -1039,7 +1189,7 @@ export function registerDevTask(ctx) {
|
|
|
1039
1189
|
overwrite: { type: 'boolean', description: 'Allow replacing an existing AGENTS.md (init apply); triggers human approval.' },
|
|
1040
1190
|
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
1191
|
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
|
|
1192
|
+
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
1193
|
justification: { type: 'string', description: 'Required with sandbox_permissions: one sentence for the user explaining why this exact operation needs the wider access.' },
|
|
1044
1194
|
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
1195
|
},
|
|
@@ -1119,6 +1269,13 @@ export function registerDevTask(ctx) {
|
|
|
1119
1269
|
if (phase !== 'propose' && phase !== 'apply')
|
|
1120
1270
|
throw new Error('init_project phase must be inspect, propose, or apply');
|
|
1121
1271
|
const resources = validateInitResources(a.resources);
|
|
1272
|
+
for (const resource of resources) {
|
|
1273
|
+
if (resource.kind !== 'skill' || resource.name !== `${inventory.project_name}-project-map`)
|
|
1274
|
+
continue;
|
|
1275
|
+
const gaps = projectMapCoverageGaps(resource.content, inventory);
|
|
1276
|
+
if (gaps.length)
|
|
1277
|
+
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.`);
|
|
1278
|
+
}
|
|
1122
1279
|
const metaRaw = await readText(fs, '.dsh/meta.json', cwd);
|
|
1123
1280
|
const meta = await readAdaptiveConfig(fs, cwd);
|
|
1124
1281
|
const createdRules = new Set(resources.filter(resource => resource.kind === 'rule').map(resource => resource.name));
|
|
@@ -1142,11 +1299,13 @@ export function registerDevTask(ctx) {
|
|
|
1142
1299
|
}
|
|
1143
1300
|
}
|
|
1144
1301
|
const metaHash = hashText(metaRaw ?? '');
|
|
1145
|
-
const
|
|
1302
|
+
const inventoryHash = hashText(JSON.stringify(inventory));
|
|
1303
|
+
const proposalHash = hashText(JSON.stringify(resources) + '\n' + metaHash + '\n' + inventoryHash);
|
|
1146
1304
|
if (phase === 'propose')
|
|
1147
1305
|
return JSON.stringify({ inventory, resources, files_to_create: paths,
|
|
1306
|
+
project_map_coverage: projectMapCoveragePaths(inventory),
|
|
1148
1307
|
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);
|
|
1308
|
+
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
1309
|
if (a.expected_hash !== proposalHash || a.existing_hash !== metaHash) {
|
|
1151
1310
|
throw new Error('init_project draft or meta config changed since propose; inspect and propose again');
|
|
1152
1311
|
}
|
|
@@ -1171,7 +1330,9 @@ export function registerDevTask(ctx) {
|
|
|
1171
1330
|
}
|
|
1172
1331
|
const info = await fs.lstat('.dsh/meta.json', { cwd });
|
|
1173
1332
|
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,
|
|
1333
|
+
return JSON.stringify({ created: [...paths, '.dsh/meta.json'], bindings,
|
|
1334
|
+
team_files: [...paths, '.dsh/meta.json'],
|
|
1335
|
+
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
1336
|
}
|
|
1176
1337
|
if (a.operation === 'init') {
|
|
1177
1338
|
const phase = a.phase ?? 'inspect';
|
|
@@ -1270,6 +1431,10 @@ export function registerDevTask(ctx) {
|
|
|
1270
1431
|
throw new Error(`high_risk task cannot run on flow "${resolved.flow}" — it lacks the required capabilities ` +
|
|
1271
1432
|
`(${HIGH_RISK_REQUIRED_CAPABILITIES.join(', ')}). Use the standard flow or lower the task risk.`);
|
|
1272
1433
|
}
|
|
1434
|
+
const sonarPolicy = adaptiveConfig === undefined ? undefined : sonarPolicyFor(adaptiveConfig);
|
|
1435
|
+
if (sonarPolicy?.source === 'ide-local' && resolved.config.stages.includes('代码审核')) {
|
|
1436
|
+
resolved.config.commit = { ...resolved.config.commit, checkpoints: ['代码审核'] };
|
|
1437
|
+
}
|
|
1273
1438
|
// Freeze the resolved skill and rule bodies now. A name alone cannot keep an
|
|
1274
1439
|
// in-flight task stable: editing a rule the task uses would otherwise change
|
|
1275
1440
|
// what the task is doing without the task saying so.
|
|
@@ -1285,7 +1450,6 @@ export function registerDevTask(ctx) {
|
|
|
1285
1450
|
resources: frozen.resources,
|
|
1286
1451
|
};
|
|
1287
1452
|
const root = await detectRoot(projectProbe(fs), cwd ?? '');
|
|
1288
|
-
const sonarPolicy = adaptiveConfig === undefined ? undefined : sonarPolicyFor(adaptiveConfig);
|
|
1289
1453
|
const state = newTask({
|
|
1290
1454
|
id: a.task_id,
|
|
1291
1455
|
title: a.title,
|
|
@@ -1351,6 +1515,7 @@ export function registerDevTask(ctx) {
|
|
|
1351
1515
|
...skillResourceBlockers(missingSkillFiles, state.stage),
|
|
1352
1516
|
...resourceBlockers(missingRules, state.stage), ...staleEvidence, ...await sonarBlockers(fs, state, cwd)]
|
|
1353
1517
|
: [];
|
|
1518
|
+
const planItemBlockers = taskPlanItemBlockers(state);
|
|
1354
1519
|
return JSON.stringify({
|
|
1355
1520
|
id: state.id,
|
|
1356
1521
|
stage: state.stage,
|
|
@@ -1365,6 +1530,7 @@ export function registerDevTask(ctx) {
|
|
|
1365
1530
|
solution_confirmed: state.solution_confirmed,
|
|
1366
1531
|
items_done: `${state.items.filter(i => i.status === 'done').length}/${state.items.length}`,
|
|
1367
1532
|
items: state.items,
|
|
1533
|
+
task_plan_item_blockers: planItemBlockers,
|
|
1368
1534
|
skill_obligations: obligationStages(state, workflow).map(stage => {
|
|
1369
1535
|
const bindings = workflow.stage_bindings?.[stage]?.skills ?? [];
|
|
1370
1536
|
const skills = bindings.map(entry => state.complexity !== undefined || entry.skill.source === 'codex-project'
|
|
@@ -1402,10 +1568,12 @@ export function registerDevTask(ctx) {
|
|
|
1402
1568
|
review: state.review,
|
|
1403
1569
|
sonar: state.sonar_policy === undefined ? { enabled: false } : {
|
|
1404
1570
|
enabled: true, mode: state.sonar_policy.mode, project_key: state.sonar_policy.project_key,
|
|
1571
|
+
source: state.sonar_policy.source ?? 'ci', reference_branch: state.sonar_policy.reference_branch,
|
|
1405
1572
|
audit: state.sonar_audit ?? null, blockers: await sonarBlockers(fs, state, cwd),
|
|
1406
1573
|
prior_audits: state.sonar_history?.length ?? 0,
|
|
1407
1574
|
},
|
|
1408
1575
|
learned_rules: state.learned_rules ?? [],
|
|
1576
|
+
rule_learning_candidates: ruleLearningCandidates(state),
|
|
1409
1577
|
artifact_requirements: workflow.artifacts.filter(def => def.stage === state.stage).map(def => ({
|
|
1410
1578
|
id: def.id, name: def.name, fields: def.fields,
|
|
1411
1579
|
missing_fields: def.fields.filter(field => !(state.artifacts[def.id]?.[field] ?? '').trim()),
|
|
@@ -1436,25 +1604,84 @@ export function registerDevTask(ctx) {
|
|
|
1436
1604
|
if (!state.verification.passed || (await evidenceBlockers(fs, state, workflow, cwd)).length) {
|
|
1437
1605
|
throw new Error('functional verification must pass on the current files before sonar_check');
|
|
1438
1606
|
}
|
|
1439
|
-
if (!state.commits.some(commit => commit.hash !== undefined)) {
|
|
1440
|
-
throw new Error('sonar_check requires a recorded Git commit
|
|
1607
|
+
if (state.sonar_policy.source !== 'ide-local' && !state.commits.some(commit => commit.hash !== undefined)) {
|
|
1608
|
+
throw new Error('sonar_check requires a recorded Git commit at the 测试 checkpoint; CI mode also requires a push');
|
|
1441
1609
|
}
|
|
1442
|
-
if (!a.ce_task_id)
|
|
1443
|
-
throw new Error('sonar_check requires ce_task_id from this task\'s completed CI scan');
|
|
1444
1610
|
const target = state.sonar_policy.mode === 'branch' ? state.branch : a.pull_request;
|
|
1445
1611
|
if (!target)
|
|
1446
1612
|
throw new Error('pull-request SonarQube audit requires pull_request');
|
|
1447
|
-
const
|
|
1613
|
+
const credentialProvider = ctx.get('credentials');
|
|
1614
|
+
const token = await resolveSonarToken(credentialProvider, state.root, process.env[state.sonar_policy.token_env]);
|
|
1615
|
+
const scopeBefore = await scopeFingerprint(fs, state, cwd);
|
|
1616
|
+
if (state.sonar_policy.source === 'ide-local') {
|
|
1617
|
+
if (a.ce_task_id)
|
|
1618
|
+
throw new Error('本地规则审核不使用 CI ce_task_id');
|
|
1619
|
+
if (!state.files.length)
|
|
1620
|
+
throw new Error('本地规则审核需要先登记当前任务的文件范围');
|
|
1621
|
+
const root = state.root ?? cwd;
|
|
1622
|
+
if (!root)
|
|
1623
|
+
throw new Error('本地规则审核需要项目工作区');
|
|
1624
|
+
const audit = await inspectLocalRules(state.sonar_policy, token, root, scopeBefore, exec.signal, state.files);
|
|
1625
|
+
if (await scopeFingerprint(fs, state, cwd) !== scopeBefore) {
|
|
1626
|
+
throw new Error('本地规则审核期间代码发生变化,请重新测试和审核');
|
|
1627
|
+
}
|
|
1628
|
+
if (state.sonar_audit !== undefined)
|
|
1629
|
+
state.sonar_history = [...(state.sonar_history ?? []), state.sonar_audit].slice(-20);
|
|
1630
|
+
state.sonar_audit = audit;
|
|
1631
|
+
state.review.outcome = 'pending';
|
|
1632
|
+
const mode = await resolveWriteMode(ctx, a, exec);
|
|
1633
|
+
await writeSonarReport(fs, state, audit, cwd, mode);
|
|
1634
|
+
await writeTask(fs, state, cwd, mode);
|
|
1635
|
+
return JSON.stringify({ gate: audit.gate, blocking: audit.blocking, findings: audit.findings,
|
|
1636
|
+
uncovered_files: audit.uncovered_files ?? [], report_path: audit.report_path,
|
|
1637
|
+
rule_learning_candidates: ruleLearningCandidates(state),
|
|
1638
|
+
next_action: audit.gate === 'OK' ? '继续代码审核;检查历史阻断问题是否有可复用的修复经验,再决定是否通过 learn_rule 提议项目 Rule。误报或一次性问题不要沉淀。'
|
|
1639
|
+
: '修复新增代码问题,并处理未覆盖文件;重新测试后再次运行 sonar_check。' }, null, 2);
|
|
1640
|
+
}
|
|
1641
|
+
let ceTaskId = a.ce_task_id;
|
|
1642
|
+
if (state.sonar_policy.source === 'local') {
|
|
1643
|
+
if (ceTaskId)
|
|
1644
|
+
throw new Error('local sonar_check runs a fresh scan; do not provide a CI ce_task_id');
|
|
1645
|
+
const root = state.root ?? cwd;
|
|
1646
|
+
if (!root)
|
|
1647
|
+
throw new Error('local SonarQube scan needs a project workspace');
|
|
1648
|
+
const head = await runVerificationCommand(ctx, 'git rev-parse HEAD', root, exec);
|
|
1649
|
+
const branch = await runVerificationCommand(ctx, 'git branch --show-current', root, exec);
|
|
1650
|
+
const status = await runVerificationCommand(ctx, 'git status --porcelain --untracked-files=all', root, exec);
|
|
1651
|
+
const lastCommit = state.commits.findLast(entry => entry.hash !== undefined)?.hash;
|
|
1652
|
+
if (head.exit_code !== 0 || branch.exit_code !== 0 || status.exit_code !== 0 || !lastCommit
|
|
1653
|
+
|| !head.stdout.trim().startsWith(lastCommit) || branch.stdout.trim() !== state.branch) {
|
|
1654
|
+
throw new Error('local SonarQube scan requires the recorded commit checked out on the task branch');
|
|
1655
|
+
}
|
|
1656
|
+
const dirty = status.stdout.split(/\r?\n/u).filter(line => line.trim() && !/^..\s+\.dsh[\\/]/u.test(line));
|
|
1657
|
+
if (dirty.length)
|
|
1658
|
+
throw new Error('local SonarQube scan requires a clean working tree outside .dsh; commit or remove unrecorded changes first');
|
|
1659
|
+
await assertLocalSonarReady(state.sonar_policy, token);
|
|
1660
|
+
const defaultCommand = await readText(fs, 'pom.xml', root) === undefined ? 'sonar-scanner'
|
|
1661
|
+
: 'mvn org.sonarsource.scanner.maven:sonar-maven-plugin:5.5.0.6356:sonar';
|
|
1662
|
+
const approvedMode = a.sandbox_permissions === undefined ? undefined : await resolveWriteMode(ctx, { ...a, command: state.sonar_policy.scan_command?.trim() || defaultCommand }, exec);
|
|
1663
|
+
ceTaskId = await runLocalSonar(ctx, state.sonar_policy, token, root, state.branch, exec, defaultCommand, approvedMode);
|
|
1664
|
+
}
|
|
1665
|
+
if (!ceTaskId)
|
|
1666
|
+
throw new Error('sonar_check requires ce_task_id from this task\'s completed CI scan');
|
|
1667
|
+
if (state.sonar_policy.source === 'local' && await scopeFingerprint(fs, state, cwd) !== scopeBefore) {
|
|
1668
|
+
throw new Error('local SonarQube scan changed task files; rerun functional verification before auditing');
|
|
1669
|
+
}
|
|
1670
|
+
const audit = await inspectCompletedSonar(state.sonar_policy, token, ceTaskId, target, scopeBefore, exec.signal);
|
|
1448
1671
|
audit.commit_hash = state.commits.findLast(entry => entry.hash !== undefined).hash;
|
|
1449
1672
|
if (state.sonar_audit !== undefined)
|
|
1450
1673
|
state.sonar_history = [...(state.sonar_history ?? []), state.sonar_audit].slice(-20);
|
|
1451
1674
|
state.sonar_audit = audit;
|
|
1452
1675
|
state.review.outcome = 'pending';
|
|
1453
|
-
|
|
1454
|
-
|
|
1676
|
+
const mode = await resolveWriteMode(ctx, a, exec);
|
|
1677
|
+
await writeSonarReport(fs, state, audit, cwd, mode);
|
|
1678
|
+
await writeTask(fs, state, cwd, mode);
|
|
1679
|
+
return JSON.stringify({ gate: audit.gate, ...(audit.server_gate ? { server_gate: audit.server_gate } : {}),
|
|
1680
|
+
blocking: audit.blocking, findings: audit.findings, report_path: audit.report_path,
|
|
1681
|
+
rule_learning_candidates: ruleLearningCandidates(state),
|
|
1455
1682
|
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
|
|
1683
|
+
? 'Review prior blocking issues for reusable fixes with learn_rule; do not learn false positives. Continue code review, then record review pass.'
|
|
1684
|
+
: 'Record review blocked, fix findings, rerun tests and the scan, then call sonar_check again.' }, null, 2);
|
|
1458
1685
|
}
|
|
1459
1686
|
if (a.operation === 'learn_rule') {
|
|
1460
1687
|
const issue = [state.sonar_audit, ...(state.sonar_history ?? [])].filter(entry => entry !== undefined)
|
|
@@ -1665,7 +1892,7 @@ export function registerDevTask(ctx) {
|
|
|
1665
1892
|
item.status = 'doing';
|
|
1666
1893
|
delete item.review;
|
|
1667
1894
|
item.dispatch = { description: a.description ?? '', at: new Date().toISOString() };
|
|
1668
|
-
note = `item "${item.id}" is doing;
|
|
1895
|
+
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
1896
|
break;
|
|
1670
1897
|
}
|
|
1671
1898
|
case 'review_item': {
|
|
@@ -1684,6 +1911,9 @@ export function registerDevTask(ctx) {
|
|
|
1684
1911
|
}
|
|
1685
1912
|
case 'advance': {
|
|
1686
1913
|
const target = a.target_stage ?? '';
|
|
1914
|
+
const planItemBlockers = taskPlanItemBlockers(state);
|
|
1915
|
+
if (planItemBlockers.length)
|
|
1916
|
+
throw new Error(planItemBlockers.join('; '));
|
|
1687
1917
|
await assertFreshEvidence(fs, state, workflow, cwd);
|
|
1688
1918
|
const missingSkills = skillBlockers(state, workflow, session);
|
|
1689
1919
|
if (missingSkills.length)
|