projscan 4.13.0 → 4.15.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/README.md +90 -24
- package/dist/cli/commands/evidencePack.js +2 -0
- package/dist/cli/commands/evidencePack.js.map +1 -1
- package/dist/cli/commands/prove.js +253 -23
- package/dist/cli/commands/prove.js.map +1 -1
- package/dist/cli/commands/startConsole.d.ts +2 -2
- package/dist/cli/commands/startConsole.js +2 -260
- package/dist/cli/commands/startConsole.js.map +1 -1
- package/dist/cli/commands/startConsoleExecution.d.ts +5 -0
- package/dist/cli/commands/startConsoleExecution.js +108 -0
- package/dist/cli/commands/startConsoleExecution.js.map +1 -0
- package/dist/cli/commands/startConsoleMission.d.ts +6 -0
- package/dist/cli/commands/startConsoleMission.js +157 -0
- package/dist/cli/commands/startConsoleMission.js.map +1 -0
- package/dist/cli/commands/startMissionBundle.js +24 -27
- package/dist/cli/commands/startMissionBundle.js.map +1 -1
- package/dist/core/adoption.d.ts +8 -81
- package/dist/core/adoption.js +4 -549
- package/dist/core/adoption.js.map +1 -1
- package/dist/core/adoptionFirstRunDiagnostics.d.ts +20 -0
- package/dist/core/adoptionFirstRunDiagnostics.js +240 -0
- package/dist/core/adoptionFirstRunDiagnostics.js.map +1 -0
- package/dist/core/adoptionMcpConfig.d.ts +27 -0
- package/dist/core/adoptionMcpConfig.js +123 -0
- package/dist/core/adoptionMcpConfig.js.map +1 -0
- package/dist/core/adoptionMcpDoctor.d.ts +23 -0
- package/dist/core/adoptionMcpDoctor.js +87 -0
- package/dist/core/adoptionMcpDoctor.js.map +1 -0
- package/dist/core/adoptionWorkflowRecipes.d.ts +14 -0
- package/dist/core/adoptionWorkflowRecipes.js +110 -0
- package/dist/core/adoptionWorkflowRecipes.js.map +1 -0
- package/dist/core/bugHunt.js +26 -255
- package/dist/core/bugHunt.js.map +1 -1
- package/dist/core/bugHuntPreflightFindings.d.ts +2 -1
- package/dist/core/bugHuntPreflightFindings.js +20 -0
- package/dist/core/bugHuntPreflightFindings.js.map +1 -1
- package/dist/core/bugHuntReportAssembly.d.ts +20 -0
- package/dist/core/bugHuntReportAssembly.js +179 -0
- package/dist/core/bugHuntReportAssembly.js.map +1 -0
- package/dist/core/bugHuntSourceFindings.d.ts +3 -0
- package/dist/core/bugHuntSourceFindings.js +61 -0
- package/dist/core/bugHuntSourceFindings.js.map +1 -0
- package/dist/core/dogfood.js +4 -393
- package/dist/core/dogfood.js.map +1 -1
- package/dist/core/dogfoodMarketValidation.d.ts +5 -0
- package/dist/core/dogfoodMarketValidation.js +265 -0
- package/dist/core/dogfoodMarketValidation.js.map +1 -0
- package/dist/core/dogfoodRepoEvaluation.d.ts +4 -0
- package/dist/core/dogfoodRepoEvaluation.js +137 -0
- package/dist/core/dogfoodRepoEvaluation.js.map +1 -0
- package/dist/core/evidenceComment.js +50 -13
- package/dist/core/evidenceComment.js.map +1 -1
- package/dist/core/feedback.js +2 -252
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/feedbackIntakeClassifier.d.ts +2 -0
- package/dist/core/feedbackIntakeClassifier.js +255 -0
- package/dist/core/feedbackIntakeClassifier.js.map +1 -0
- package/dist/core/intentRouterCatalog.js +34 -0
- package/dist/core/intentRouterCatalog.js.map +1 -1
- package/dist/core/intentRouterKeywordToolGuards.js +2 -46
- package/dist/core/intentRouterKeywordToolGuards.js.map +1 -1
- package/dist/core/intentRouterKeywordWeights.js +13 -28
- package/dist/core/intentRouterKeywordWeights.js.map +1 -1
- package/dist/core/intentRouterProductGuardSignals.d.ts +3 -0
- package/dist/core/intentRouterProductGuardSignals.js +59 -0
- package/dist/core/intentRouterProductGuardSignals.js.map +1 -0
- package/dist/core/intentRouterWorkflowKeywordWeights.js +29 -0
- package/dist/core/intentRouterWorkflowKeywordWeights.js.map +1 -1
- package/dist/core/markdownSafety.d.ts +3 -0
- package/dist/core/markdownSafety.js +14 -0
- package/dist/core/markdownSafety.js.map +1 -0
- package/dist/core/preflight.d.ts +2 -0
- package/dist/core/preflight.js.map +1 -1
- package/dist/core/preflightChangedFiles.d.ts +2 -0
- package/dist/core/preflightChangedFiles.js +1 -1
- package/dist/core/preflightChangedFiles.js.map +1 -1
- package/dist/core/preflightInputs.d.ts +2 -0
- package/dist/core/preflightInputs.js +5 -2
- package/dist/core/preflightInputs.js.map +1 -1
- package/dist/core/proofLedger.d.ts +6 -1
- package/dist/core/proofLedger.js +174 -15
- package/dist/core/proofLedger.js.map +1 -1
- package/dist/core/proofReplay.d.ts +9 -0
- package/dist/core/proofReplay.js +164 -0
- package/dist/core/proofReplay.js.map +1 -0
- package/dist/core/proofSufficiency.d.ts +19 -0
- package/dist/core/proofSufficiency.js +425 -0
- package/dist/core/proofSufficiency.js.map +1 -0
- package/dist/core/prove.d.ts +8 -0
- package/dist/core/prove.js +578 -88
- package/dist/core/prove.js.map +1 -1
- package/dist/core/qualityScorecard.js +8 -238
- package/dist/core/qualityScorecard.js.map +1 -1
- package/dist/core/qualityScorecardDimensions.d.ts +14 -0
- package/dist/core/qualityScorecardDimensions.js +99 -0
- package/dist/core/qualityScorecardDimensions.js.map +1 -0
- package/dist/core/qualityScorecardRisks.d.ts +8 -0
- package/dist/core/qualityScorecardRisks.js +107 -0
- package/dist/core/qualityScorecardRisks.js.map +1 -0
- package/dist/core/qualityScorecardSignals.d.ts +20 -0
- package/dist/core/qualityScorecardSignals.js +59 -0
- package/dist/core/qualityScorecardSignals.js.map +1 -0
- package/dist/core/releaseEvidence.d.ts +1 -0
- package/dist/core/releaseEvidence.js +15 -40
- package/dist/core/releaseEvidence.js.map +1 -1
- package/dist/core/releaseEvidenceBaseline.js +4 -1
- package/dist/core/releaseEvidenceBaseline.js.map +1 -1
- package/dist/core/releaseEvidenceProofReceipt.d.ts +6 -0
- package/dist/core/releaseEvidenceProofReceipt.js +140 -0
- package/dist/core/releaseEvidenceProofReceipt.js.map +1 -0
- package/dist/core/releaseEvidenceVerdict.d.ts +5 -2
- package/dist/core/releaseEvidenceVerdict.js +39 -1
- package/dist/core/releaseEvidenceVerdict.js.map +1 -1
- package/dist/core/repositoryScanner.d.ts +1 -0
- package/dist/core/repositoryScanner.js +5 -4
- package/dist/core/repositoryScanner.js.map +1 -1
- package/dist/core/sessionResources.d.ts +14 -2
- package/dist/core/sessionResources.js +3 -3
- package/dist/core/sessionResources.js.map +1 -1
- package/dist/core/startFixedRouteCriteria.js +4 -0
- package/dist/core/startFixedRouteCriteria.js.map +1 -1
- package/dist/core/startInputs.d.ts +1 -1
- package/dist/core/startIntentTargets.d.ts +1 -1
- package/dist/core/startIntentTargets.js +1 -16
- package/dist/core/startIntentTargets.js.map +1 -1
- package/dist/core/startMissionInputStatusPolicy.d.ts +7 -0
- package/dist/core/startMissionInputStatusPolicy.js +74 -0
- package/dist/core/startMissionInputStatusPolicy.js.map +1 -0
- package/dist/core/startMissionPolicy.d.ts +6 -15
- package/dist/core/startMissionPolicy.js +4 -305
- package/dist/core/startMissionPolicy.js.map +1 -1
- package/dist/core/startMissionProofPolicy.d.ts +6 -0
- package/dist/core/startMissionProofPolicy.js +84 -0
- package/dist/core/startMissionProofPolicy.js.map +1 -0
- package/dist/core/startMissionRiskPolicy.d.ts +4 -0
- package/dist/core/startMissionRiskPolicy.js +85 -0
- package/dist/core/startMissionRiskPolicy.js.map +1 -0
- package/dist/core/startMissionRoutingPolicy.d.ts +6 -0
- package/dist/core/startMissionRoutingPolicy.js +67 -0
- package/dist/core/startMissionRoutingPolicy.js.map +1 -0
- package/dist/core/startMode.d.ts +1 -2
- package/dist/core/startMode.js +4 -151
- package/dist/core/startMode.js.map +1 -1
- package/dist/core/startModeIntentPolicy.d.ts +12 -0
- package/dist/core/startModeIntentPolicy.js +41 -0
- package/dist/core/startModeIntentPolicy.js.map +1 -0
- package/dist/core/startModeRoutingPolicy.d.ts +4 -0
- package/dist/core/startModeRoutingPolicy.js +117 -0
- package/dist/core/startModeRoutingPolicy.js.map +1 -0
- package/dist/core/startRouteActions.js +5 -0
- package/dist/core/startRouteActions.js.map +1 -1
- package/dist/core/startSearchQueryTargets.d.ts +1 -0
- package/dist/core/startSearchQueryTargets.js +17 -0
- package/dist/core/startSearchQueryTargets.js.map +1 -0
- package/dist/core/workplan.d.ts +3 -2
- package/dist/core/workplan.js +11 -585
- package/dist/core/workplan.js.map +1 -1
- package/dist/core/workplanCoordinationTasks.d.ts +3 -0
- package/dist/core/workplanCoordinationTasks.js +82 -0
- package/dist/core/workplanCoordinationTasks.js.map +1 -0
- package/dist/core/workplanModeTasks.d.ts +2 -0
- package/dist/core/workplanModeTasks.js +192 -0
- package/dist/core/workplanModeTasks.js.map +1 -0
- package/dist/core/workplanPreflightTasks.d.ts +2 -0
- package/dist/core/workplanPreflightTasks.js +126 -0
- package/dist/core/workplanPreflightTasks.js.map +1 -0
- package/dist/core/workplanQualitySignals.d.ts +7 -0
- package/dist/core/workplanQualitySignals.js +63 -0
- package/dist/core/workplanQualitySignals.js.map +1 -0
- package/dist/core/workplanReport.d.ts +4 -0
- package/dist/core/workplanReport.js +79 -0
- package/dist/core/workplanReport.js.map +1 -0
- package/dist/core/workplanRiskOwnership.d.ts +5 -0
- package/dist/core/workplanRiskOwnership.js +97 -0
- package/dist/core/workplanRiskOwnership.js.map +1 -0
- package/dist/core/workplanSuggestedActions.d.ts +2 -0
- package/dist/core/workplanSuggestedActions.js +43 -0
- package/dist/core/workplanSuggestedActions.js.map +1 -0
- package/dist/mcp/tools/prove.js +24 -18
- package/dist/mcp/tools/prove.js.map +1 -1
- package/dist/projscan-sbom.cdx.json +6 -6
- package/dist/tool-manifest.json +3 -3
- package/dist/types/config.d.ts +15 -0
- package/dist/types/evidencePack.d.ts +21 -0
- package/dist/types/proofLedger.d.ts +1 -1
- package/dist/types/prove.d.ts +96 -1
- package/dist/utils/changedFiles.js +57 -16
- package/dist/utils/changedFiles.js.map +1 -1
- package/dist/utils/config.js +2 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/configProofRecipes.d.ts +2 -0
- package/dist/utils/configProofRecipes.js +91 -0
- package/dist/utils/configProofRecipes.js.map +1 -0
- package/docs/GUIDE.md +145 -25
- package/package.json +1 -1
package/dist/core/workplan.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { fixFirstFromWorkplanRisk, fixFirstFromWorkplanTask } from './fixFirst.js';
|
|
2
2
|
import { computePreflight } from './preflight.js';
|
|
3
|
-
import { computeQualityScorecard } from './qualityScorecard.js';
|
|
4
3
|
import { buildRiskNow } from './sessionResources.js';
|
|
5
4
|
import { loadOwnership } from './ownership.js';
|
|
6
|
-
import {
|
|
5
|
+
import { buildCoordination, tasksFromCoordination } from './workplanCoordinationTasks.js';
|
|
6
|
+
import { modeTasks } from './workplanModeTasks.js';
|
|
7
|
+
import { tasksFromPreflight } from './workplanPreflightTasks.js';
|
|
8
|
+
import { safeQualitySignals } from './workplanQualitySignals.js';
|
|
9
|
+
import { buildWorkplanHandoffPayload, rankWorkplanTasks, summarizeWorkplan, } from './workplanReport.js';
|
|
10
|
+
import { annotateTasksWithOwners, annotateTopRisksWithOwners, buildTopRisks } from './workplanRiskOwnership.js';
|
|
11
|
+
import { buildWorkplanSuggestedActions } from './workplanSuggestedActions.js';
|
|
7
12
|
const DEFAULT_MAX_TASKS = 8;
|
|
8
13
|
const MAX_TOP_RISKS = 8;
|
|
9
14
|
const MAX_COORDINATION_FILES = 20;
|
|
10
|
-
const HANDOFF_LIMIT = 320;
|
|
11
15
|
const WORKPLAN_MODES = [
|
|
12
16
|
'before_edit',
|
|
13
17
|
'before_commit',
|
|
@@ -33,7 +37,7 @@ export async function computeWorkplan(rootPath, options = {}) {
|
|
|
33
37
|
const [riskNow, ownership, qualitySignals] = await Promise.all([
|
|
34
38
|
safeRiskNow(rootPath),
|
|
35
39
|
loadOwnership(rootPath).catch(() => undefined),
|
|
36
|
-
safeQualitySignals(rootPath, mode),
|
|
40
|
+
safeQualitySignals(rootPath, mode, MAX_TOP_RISKS),
|
|
37
41
|
]);
|
|
38
42
|
const coordination = buildCoordination(preflight.verdict, riskNow.touchedFiles, riskNow.conflicts);
|
|
39
43
|
const modeFiles = unique([...coordination.touchedFiles, ...qualitySignals.files]);
|
|
@@ -59,48 +63,12 @@ export async function computeWorkplan(rootPath, options = {}) {
|
|
|
59
63
|
tasks: limitedTasks,
|
|
60
64
|
...(fixFirst ? { fixFirst } : {}),
|
|
61
65
|
coordination,
|
|
62
|
-
suggestedNextActions:
|
|
63
|
-
...preflight.suggestedNextActions,
|
|
64
|
-
...limitedTasks.flatMap((task) => taskToSuggestedActions(task)),
|
|
65
|
-
]),
|
|
66
|
+
suggestedNextActions: buildWorkplanSuggestedActions(preflight.suggestedNextActions, limitedTasks),
|
|
66
67
|
...(truncated ? { truncated: true } : {}),
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
export function buildWorkplanHandoff(report) {
|
|
70
|
-
|
|
71
|
-
const verificationCommands = unique(report.tasks.flatMap((task) => task.verification.commands)).slice(0, 12);
|
|
72
|
-
return {
|
|
73
|
-
summary: report.summary,
|
|
74
|
-
verdict: report.verdict,
|
|
75
|
-
mode: report.mode,
|
|
76
|
-
next,
|
|
77
|
-
verificationCommands,
|
|
78
|
-
coordination: report.coordination,
|
|
79
|
-
markdown: renderWorkplanHandoffMarkdown(report, next, verificationCommands),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
function renderWorkplanHandoffMarkdown(report, next, verificationCommands) {
|
|
83
|
-
const lines = [
|
|
84
|
-
'# Agent Handoff',
|
|
85
|
-
'',
|
|
86
|
-
`**Mode:** ${report.mode}`,
|
|
87
|
-
`**Verdict:** ${report.verdict}`,
|
|
88
|
-
'',
|
|
89
|
-
report.summary,
|
|
90
|
-
'',
|
|
91
|
-
'## Next',
|
|
92
|
-
...(next.length > 0 ? next.map((item) => `- ${item}`) : ['- Preserve the current baseline.']),
|
|
93
|
-
'',
|
|
94
|
-
'## Verification',
|
|
95
|
-
...(verificationCommands.length > 0
|
|
96
|
-
? verificationCommands.map((command) => `- \`${command}\``)
|
|
97
|
-
: ['- `projscan preflight --format json`']),
|
|
98
|
-
'',
|
|
99
|
-
'## Coordination',
|
|
100
|
-
`- ${report.coordination.recommendedNextAgent}`,
|
|
101
|
-
...report.coordination.touchedFiles.slice(0, 10).map((file) => `- touched: ${file}`),
|
|
102
|
-
];
|
|
103
|
-
return `${lines.join('\n')}\n`;
|
|
71
|
+
return buildWorkplanHandoffPayload(report);
|
|
104
72
|
}
|
|
105
73
|
function modeToPreflightMode(mode) {
|
|
106
74
|
if (mode === 'before_commit' || mode === 'before_merge' || mode === 'before_edit')
|
|
@@ -127,543 +95,7 @@ async function safeRiskNow(rootPath) {
|
|
|
127
95
|
};
|
|
128
96
|
}
|
|
129
97
|
}
|
|
130
|
-
|
|
131
|
-
if (mode !== 'bug_hunt')
|
|
132
|
-
return emptyQualitySignals();
|
|
133
|
-
try {
|
|
134
|
-
const report = await computeQualityScorecard(rootPath, { maxRisks: MAX_TOP_RISKS });
|
|
135
|
-
const topRisks = report.topRisks
|
|
136
|
-
.map(qualityRiskToWorkplanRisk)
|
|
137
|
-
.filter((risk) => risk !== undefined);
|
|
138
|
-
return {
|
|
139
|
-
files: unique(report.topRisks.flatMap((risk) => risk.files)),
|
|
140
|
-
evidence: topRisks.map((risk) => ({
|
|
141
|
-
source: risk.source,
|
|
142
|
-
message: risk.message,
|
|
143
|
-
...(risk.severity ? { severity: risk.severity } : {}),
|
|
144
|
-
...(risk.file ? { file: risk.file } : {}),
|
|
145
|
-
...(risk.issueId ? { issueId: risk.issueId } : {}),
|
|
146
|
-
...(risk.tool ? { tool: risk.tool } : {}),
|
|
147
|
-
})),
|
|
148
|
-
topRisks,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
return emptyQualitySignals();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
function emptyQualitySignals() {
|
|
156
|
-
return { files: [], evidence: [], topRisks: [] };
|
|
157
|
-
}
|
|
158
|
-
function qualityRiskToWorkplanRisk(risk) {
|
|
159
|
-
if (risk.files.length === 0)
|
|
160
|
-
return undefined;
|
|
161
|
-
const tool = toolFromCommand(risk.command);
|
|
162
|
-
return {
|
|
163
|
-
source: workplanSourceFromQualityRisk(risk.source),
|
|
164
|
-
message: risk.title,
|
|
165
|
-
priority: risk.priority,
|
|
166
|
-
file: risk.files[0],
|
|
167
|
-
...(tool ? { tool } : {}),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
function workplanSourceFromQualityRisk(source) {
|
|
171
|
-
if (source === 'hotspot')
|
|
172
|
-
return 'hotspots';
|
|
173
|
-
if (source === 'coordination')
|
|
174
|
-
return 'coordination';
|
|
175
|
-
return 'doctor';
|
|
176
|
-
}
|
|
177
|
-
function toolFromCommand(command) {
|
|
178
|
-
if (command.startsWith('projscan file '))
|
|
179
|
-
return 'projscan_file';
|
|
180
|
-
if (command.startsWith('projscan doctor '))
|
|
181
|
-
return 'projscan_doctor';
|
|
182
|
-
if (command.startsWith('projscan session '))
|
|
183
|
-
return 'projscan_session';
|
|
184
|
-
if (command.startsWith('projscan quality-scorecard '))
|
|
185
|
-
return 'projscan_quality_scorecard';
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
function buildCoordination(verdict, touchedFiles, conflicts) {
|
|
189
|
-
const visibleTouched = touchedFiles.slice(0, MAX_COORDINATION_FILES);
|
|
190
|
-
let recommendedNextAgent = 'preflight agent: run the safety gate, then pick the first p0/p1 task';
|
|
191
|
-
if (verdict === 'block') {
|
|
192
|
-
recommendedNextAgent = 'hardening agent: resolve p0 blockers before feature work continues';
|
|
193
|
-
}
|
|
194
|
-
else if (conflicts.length > 0) {
|
|
195
|
-
recommendedNextAgent =
|
|
196
|
-
'coordination agent: inspect touched-file overlap before parallel edits continue';
|
|
197
|
-
}
|
|
198
|
-
else if (visibleTouched.length > 0) {
|
|
199
|
-
recommendedNextAgent =
|
|
200
|
-
'handoff/preflight agent: continue from touched-file context, then confirm the safety gate before editing';
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
touchedFiles: visibleTouched,
|
|
204
|
-
conflicts,
|
|
205
|
-
recommendedNextAgent,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
function tasksFromPreflight(reasons) {
|
|
209
|
-
const tasks = [];
|
|
210
|
-
const bySource = new Map();
|
|
211
|
-
for (const reason of reasons) {
|
|
212
|
-
const key = `${reason.source}:${reason.severity}`;
|
|
213
|
-
const group = bySource.get(key) ?? [];
|
|
214
|
-
group.push(reason);
|
|
215
|
-
bySource.set(key, group);
|
|
216
|
-
}
|
|
217
|
-
const supplyChain = reasons.filter((reason) => reason.source === 'supply-chain');
|
|
218
|
-
if (supplyChain.length > 0) {
|
|
219
|
-
tasks.push(makeTask({
|
|
220
|
-
id: 'wp-supply-chain-1',
|
|
221
|
-
priority: supplyChain.some((reason) => reason.severity === 'error') ? 'p0' : 'p1',
|
|
222
|
-
title: 'Resolve supply-chain trust blockers',
|
|
223
|
-
why: 'Supply-chain findings can mean install-time compromise, unsafe dependency provenance, or hidden persistence hooks. Handle these before continuing with normal product work.',
|
|
224
|
-
evidence: supplyChain.map(reasonToEvidence),
|
|
225
|
-
files: filesFromReasons(supplyChain),
|
|
226
|
-
suggestedTools: ['projscan_doctor', 'projscan_preflight'],
|
|
227
|
-
commands: ['projscan preflight --format json', 'projscan doctor --format json'],
|
|
228
|
-
expected: 'No supply-chain errors remain, and preflight no longer blocks on supply-chain evidence.',
|
|
229
|
-
}));
|
|
230
|
-
}
|
|
231
|
-
const review = reasons.filter((reason) => reason.source === 'review' || reason.source === 'taint');
|
|
232
|
-
if (review.length > 0) {
|
|
233
|
-
tasks.push(makeTask({
|
|
234
|
-
id: 'wp-review-gate',
|
|
235
|
-
priority: review.some((reason) => reason.severity === 'error') ? 'p0' : 'p1',
|
|
236
|
-
title: 'Inspect review and dataflow blockers',
|
|
237
|
-
why: 'Review, taint, and dataflow findings describe merge safety, new risky flows, and structural changes that need explicit handling before handoff.',
|
|
238
|
-
evidence: review.map(reasonToEvidence),
|
|
239
|
-
files: filesFromReasons(review),
|
|
240
|
-
suggestedTools: [
|
|
241
|
-
'projscan_review',
|
|
242
|
-
'projscan_semantic_graph',
|
|
243
|
-
'projscan_taint',
|
|
244
|
-
'projscan_dataflow',
|
|
245
|
-
],
|
|
246
|
-
commands: [
|
|
247
|
-
'projscan review --format json',
|
|
248
|
-
'projscan semantic-graph --format json',
|
|
249
|
-
'projscan dataflow --format json',
|
|
250
|
-
'projscan preflight --mode before_merge --format json',
|
|
251
|
-
],
|
|
252
|
-
expected: 'The review verdict is ok or the remaining review items are intentionally documented.',
|
|
253
|
-
}));
|
|
254
|
-
}
|
|
255
|
-
const doctor = reasons.filter((reason) => reason.source === 'doctor' || reason.source === 'plugin');
|
|
256
|
-
if (doctor.length > 0) {
|
|
257
|
-
tasks.push(makeTask({
|
|
258
|
-
id: 'wp-health-policy',
|
|
259
|
-
priority: doctor.some((reason) => reason.severity === 'error') ? 'p0' : 'p1',
|
|
260
|
-
title: 'Fix health and plugin-policy findings',
|
|
261
|
-
why: 'Health and local policy findings are the fastest path from diagnosis to concrete fixes because they point at files, issue ids, and existing fix suggestions.',
|
|
262
|
-
evidence: doctor.map(reasonToEvidence),
|
|
263
|
-
files: filesFromReasons(doctor),
|
|
264
|
-
suggestedTools: ['projscan_doctor', 'projscan_fix_suggest'],
|
|
265
|
-
commands: ['projscan doctor --format json', 'npm test'],
|
|
266
|
-
expected: 'The relevant issue ids disappear from projscan doctor and the focused test command passes.',
|
|
267
|
-
}));
|
|
268
|
-
}
|
|
269
|
-
const hotspots = reasons.filter((reason) => reason.source === 'hotspots');
|
|
270
|
-
if (hotspots.length > 0) {
|
|
271
|
-
tasks.push(makeTask({
|
|
272
|
-
id: 'wp-touched-hotspots',
|
|
273
|
-
priority: 'p1',
|
|
274
|
-
title: 'Review touched high-risk hotspots',
|
|
275
|
-
why: 'Touched hotspots combine churn, complexity, and issue density. They are where small mistakes most often become expensive regressions.',
|
|
276
|
-
evidence: hotspots.map(reasonToEvidence),
|
|
277
|
-
files: filesFromReasons(hotspots),
|
|
278
|
-
suggestedTools: ['projscan_hotspots', 'projscan_file'],
|
|
279
|
-
commands: ['projscan hotspots --format json', 'projscan file <path> --format json'],
|
|
280
|
-
expected: 'The touched hotspot has a clear owner, test target, and reduced or accepted risk.',
|
|
281
|
-
}));
|
|
282
|
-
}
|
|
283
|
-
const changed = reasons.filter((reason) => reason.source === 'changed-files' || reason.source === 'git');
|
|
284
|
-
if (changed.length > 0) {
|
|
285
|
-
tasks.push(makeTask({
|
|
286
|
-
id: 'wp-git-scope',
|
|
287
|
-
priority: 'p1',
|
|
288
|
-
title: 'Stabilize git scope for review',
|
|
289
|
-
why: 'A workplan is only useful when changed-file and base-ref evidence are reliable. Pin the base ref before trusting merge decisions.',
|
|
290
|
-
evidence: changed.map(reasonToEvidence),
|
|
291
|
-
files: [],
|
|
292
|
-
suggestedTools: ['projscan_preflight', 'projscan_review'],
|
|
293
|
-
commands: ['projscan preflight --base-ref main --format json'],
|
|
294
|
-
expected: 'Changed-file evidence is available and review can compare the intended base/head refs.',
|
|
295
|
-
}));
|
|
296
|
-
}
|
|
297
|
-
return tasks;
|
|
298
|
-
}
|
|
299
|
-
function tasksFromCoordination(coordination) {
|
|
300
|
-
if (coordination.touchedFiles.length === 0 && coordination.conflicts.length === 0)
|
|
301
|
-
return [];
|
|
302
|
-
const evidence = [
|
|
303
|
-
...coordination.touchedFiles.slice(0, 5).map((file) => ({
|
|
304
|
-
source: 'coordination',
|
|
305
|
-
file,
|
|
306
|
-
message: `session touched ${file}`,
|
|
307
|
-
})),
|
|
308
|
-
...coordination.conflicts.map((conflict) => ({
|
|
309
|
-
source: 'coordination',
|
|
310
|
-
severity: conflict.severity,
|
|
311
|
-
file: conflict.files[0],
|
|
312
|
-
message: conflict.message,
|
|
313
|
-
})),
|
|
314
|
-
];
|
|
315
|
-
return [
|
|
316
|
-
makeTask({
|
|
317
|
-
id: 'wp-session-handoff',
|
|
318
|
-
priority: coordination.conflicts.some((conflict) => conflict.severity === 'error')
|
|
319
|
-
? 'p0'
|
|
320
|
-
: 'p1',
|
|
321
|
-
title: 'Coordinate touched files before parallel work continues',
|
|
322
|
-
why: 'Session evidence tells the next agent what changed recently and which files may collide across agents.',
|
|
323
|
-
evidence,
|
|
324
|
-
files: coordination.touchedFiles,
|
|
325
|
-
suggestedTools: ['projscan_session', 'projscan://handoff', 'projscan://risk-now'],
|
|
326
|
-
commands: ['projscan session touched --format json', 'projscan handoff'],
|
|
327
|
-
expected: 'The next agent can name touched files, current overlap risks, and the first safe task.',
|
|
328
|
-
}),
|
|
329
|
-
];
|
|
330
|
-
}
|
|
331
|
-
function modeTasks(mode, verdict, touchedFiles, qualityEvidence = []) {
|
|
332
|
-
const tasks = [];
|
|
333
|
-
if (mode === 'bug_hunt') {
|
|
334
|
-
const bugHuntFiles = unique([...touchedFiles, ...filesFromEvidence(qualityEvidence)]);
|
|
335
|
-
tasks.push(makeTask({
|
|
336
|
-
id: 'wp-bug-hunt-hotspots',
|
|
337
|
-
priority: 'p1',
|
|
338
|
-
title: 'Hunt bugs in the highest-risk files',
|
|
339
|
-
why: 'The fastest polish pass starts where churn, complexity, and current issues overlap instead of scanning the whole repository equally.',
|
|
340
|
-
evidence: [
|
|
341
|
-
...qualityEvidence.slice(0, 5),
|
|
342
|
-
{
|
|
343
|
-
source: 'verification',
|
|
344
|
-
message: 'bug_hunt mode prioritizes hotspots, doctor issues, and focused tests',
|
|
345
|
-
},
|
|
346
|
-
],
|
|
347
|
-
files: bugHuntFiles,
|
|
348
|
-
suggestedTools: ['projscan_hotspots', 'projscan_file', 'projscan_doctor'],
|
|
349
|
-
commands: ['projscan hotspots --format json', 'projscan doctor --format json', 'npm test'],
|
|
350
|
-
expected: 'At least one high-risk file or issue is either fixed, covered by a focused test, or explicitly deferred with evidence.',
|
|
351
|
-
}));
|
|
352
|
-
tasks.push(makeTask({
|
|
353
|
-
id: 'wp-bug-hunt-regression-tests',
|
|
354
|
-
priority: 'p1',
|
|
355
|
-
title: 'Add or run regression tests for the risky change path',
|
|
356
|
-
why: 'Polish work only sticks when the exact failure mode is covered by a repeatable command.',
|
|
357
|
-
evidence: [
|
|
358
|
-
{
|
|
359
|
-
source: 'verification',
|
|
360
|
-
message: 'every bug hunt task should end with a reproducible verification command',
|
|
361
|
-
},
|
|
362
|
-
],
|
|
363
|
-
files: touchedFiles,
|
|
364
|
-
suggestedTools: ['projscan_review', 'projscan_coverage'],
|
|
365
|
-
commands: ['npm test', 'npm run lint'],
|
|
366
|
-
expected: 'The focused regression test fails before the fix, passes after the fix, and lint stays clean.',
|
|
367
|
-
}));
|
|
368
|
-
}
|
|
369
|
-
if (mode === 'release') {
|
|
370
|
-
tasks.push(makeTask({
|
|
371
|
-
id: 'wp-release-readiness',
|
|
372
|
-
priority: 'p0',
|
|
373
|
-
title: 'Run the local release-readiness gate',
|
|
374
|
-
why: 'Release work needs one local command that checks version metadata, changelog, tag state, release gates, SBOM, and packed install smoke before any publish action.',
|
|
375
|
-
evidence: [
|
|
376
|
-
{
|
|
377
|
-
source: 'release',
|
|
378
|
-
message: 'release mode requires release:check before tagging or dispatching workflows',
|
|
379
|
-
},
|
|
380
|
-
],
|
|
381
|
-
files: ['package.json', 'CHANGELOG.md', '.github/mcp-registry/server.json'],
|
|
382
|
-
suggestedTools: ['release:check', 'projscan_preflight'],
|
|
383
|
-
commands: ['npm run release:check'],
|
|
384
|
-
expected: 'release:check reports no blockers and prints the next release action.',
|
|
385
|
-
}));
|
|
386
|
-
tasks.push(makeTask({
|
|
387
|
-
id: 'wp-release-website',
|
|
388
|
-
priority: 'p2',
|
|
389
|
-
title: 'Prepare the local website update prompt',
|
|
390
|
-
why: 'The website update starts as local handoff text; package, GitHub Release, and MCP Registry checks wait until release approval.',
|
|
391
|
-
evidence: [
|
|
392
|
-
{
|
|
393
|
-
source: 'release',
|
|
394
|
-
message: 'website follow-up starts from local evidence-pack prompt text',
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
files: ['docs/WEBSITE-UPDATE-PROMPT.md'],
|
|
398
|
-
suggestedTools: ['projscan_evidence_pack', 'website prompt'],
|
|
399
|
-
commands: ['projscan evidence-pack --website-prompt --format json'],
|
|
400
|
-
expected: 'Website prompt is generated as local handoff text; final website update waits for release approval and shipped assets.',
|
|
401
|
-
}));
|
|
402
|
-
}
|
|
403
|
-
if (mode === 'refactor') {
|
|
404
|
-
tasks.push(makeTask({
|
|
405
|
-
id: 'wp-refactor-impact',
|
|
406
|
-
priority: verdict === 'block' ? 'p1' : 'p0',
|
|
407
|
-
title: 'Pick one hotspot and inspect blast radius',
|
|
408
|
-
why: 'A safe refactor starts with one high-risk file and an impact map, not a broad cleanup pass.',
|
|
409
|
-
evidence: [
|
|
410
|
-
{
|
|
411
|
-
source: 'verification',
|
|
412
|
-
message: 'refactor mode composes hotspots with impact before edits',
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
files: touchedFiles,
|
|
416
|
-
suggestedTools: ['projscan_hotspots', 'projscan_impact'],
|
|
417
|
-
commands: ['projscan hotspots --format json', 'projscan impact <file> --format json'],
|
|
418
|
-
expected: 'The chosen refactor target has importers, tests, and rollback scope identified before edits begin.',
|
|
419
|
-
}));
|
|
420
|
-
}
|
|
421
|
-
if (mode === 'hardening') {
|
|
422
|
-
tasks.push(makeTask({
|
|
423
|
-
id: 'wp-hardening-gate',
|
|
424
|
-
priority: verdict === 'block' ? 'p1' : 'p0',
|
|
425
|
-
title: 'Run security and supply-chain hardening checks',
|
|
426
|
-
why: 'Hardening mode should prove the repo can reject compromised dependencies, unsafe scripts, and known audit findings.',
|
|
427
|
-
evidence: [
|
|
428
|
-
{
|
|
429
|
-
source: 'supply-chain',
|
|
430
|
-
message: 'hardening mode pairs preflight evidence with release security gates',
|
|
431
|
-
},
|
|
432
|
-
],
|
|
433
|
-
files: ['package.json', 'package-lock.json'],
|
|
434
|
-
suggestedTools: [
|
|
435
|
-
'projscan_preflight',
|
|
436
|
-
'projscan_doctor',
|
|
437
|
-
'projscan_semantic_graph',
|
|
438
|
-
'npm audit',
|
|
439
|
-
],
|
|
440
|
-
commands: [
|
|
441
|
-
'projscan preflight --format json',
|
|
442
|
-
'projscan semantic-graph --format json',
|
|
443
|
-
'npm audit --audit-level=moderate',
|
|
444
|
-
],
|
|
445
|
-
expected: 'Preflight has no supply-chain blockers and npm audit reports no moderate-or-higher vulnerabilities.',
|
|
446
|
-
}));
|
|
447
|
-
}
|
|
448
|
-
if (mode === 'before_edit') {
|
|
449
|
-
tasks.push(makeTask({
|
|
450
|
-
id: 'wp-before-edit-orient',
|
|
451
|
-
priority: verdict === 'block' ? 'p1' : 'p0',
|
|
452
|
-
title: 'Orient before editing',
|
|
453
|
-
why: 'Before editing, the agent needs a compact safety verdict, touched-file context, and the first target file.',
|
|
454
|
-
evidence: [
|
|
455
|
-
{
|
|
456
|
-
source: 'verification',
|
|
457
|
-
message: 'before_edit mode starts from preflight and session context',
|
|
458
|
-
},
|
|
459
|
-
],
|
|
460
|
-
files: touchedFiles,
|
|
461
|
-
suggestedTools: ['projscan_preflight', 'projscan_session', 'projscan_hotspots'],
|
|
462
|
-
commands: ['projscan preflight --mode before_edit --format json'],
|
|
463
|
-
expected: 'The agent can explain whether it may proceed and which evidence supports the next edit.',
|
|
464
|
-
}));
|
|
465
|
-
}
|
|
466
|
-
if (mode === 'before_commit' || mode === 'before_merge') {
|
|
467
|
-
tasks.push(makeTask({
|
|
468
|
-
id: 'wp-merge-readiness',
|
|
469
|
-
priority: verdict === 'block' ? 'p1' : 'p0',
|
|
470
|
-
title: 'Prove commit and merge readiness',
|
|
471
|
-
why: 'Commit and merge gates should be based on changed-file evidence, review verdict, taint status, and focused verification commands.',
|
|
472
|
-
evidence: [
|
|
473
|
-
{
|
|
474
|
-
source: 'review',
|
|
475
|
-
message: `${mode} mode composes review, preflight, and changed-file checks`,
|
|
476
|
-
},
|
|
477
|
-
],
|
|
478
|
-
files: touchedFiles,
|
|
479
|
-
suggestedTools: ['projscan_preflight', 'projscan_review', 'projscan_semantic_graph'],
|
|
480
|
-
commands: [
|
|
481
|
-
`projscan preflight --mode ${mode} --format json`,
|
|
482
|
-
'projscan semantic-graph --format json',
|
|
483
|
-
'npm test',
|
|
484
|
-
'npm run lint',
|
|
485
|
-
],
|
|
486
|
-
expected: 'Preflight is proceed or the remaining caution/block reasons are fixed before handoff.',
|
|
487
|
-
}));
|
|
488
|
-
}
|
|
489
|
-
return tasks;
|
|
490
|
-
}
|
|
491
|
-
function makeTask(input) {
|
|
492
|
-
const files = unique(input.files.filter(Boolean)).slice(0, 12);
|
|
493
|
-
const handoffText = compact(`${input.priority.toUpperCase()} ${input.title}: ${input.why} Verify with ${input.commands.join(' && ')}.${files.length > 0 ? ` Files: ${files.join(', ')}.` : ''}`, HANDOFF_LIMIT);
|
|
494
|
-
return {
|
|
495
|
-
id: input.id,
|
|
496
|
-
priority: input.priority,
|
|
497
|
-
title: input.title,
|
|
498
|
-
why: input.why,
|
|
499
|
-
evidence: input.evidence,
|
|
500
|
-
files,
|
|
501
|
-
suggestedTools: unique(input.suggestedTools),
|
|
502
|
-
verification: {
|
|
503
|
-
commands: input.commands,
|
|
504
|
-
expected: input.expected,
|
|
505
|
-
},
|
|
506
|
-
handoffText,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
export function rankWorkplanTasks(tasks) {
|
|
510
|
-
const seen = new Set();
|
|
511
|
-
return tasks
|
|
512
|
-
.filter((task) => {
|
|
513
|
-
if (seen.has(task.id))
|
|
514
|
-
return false;
|
|
515
|
-
seen.add(task.id);
|
|
516
|
-
return true;
|
|
517
|
-
})
|
|
518
|
-
.sort((a, b) => {
|
|
519
|
-
const priority = priorityRank(a.priority) - priorityRank(b.priority);
|
|
520
|
-
if (priority !== 0)
|
|
521
|
-
return priority;
|
|
522
|
-
const evidence = strongestEvidenceRank(a.evidence) - strongestEvidenceRank(b.evidence);
|
|
523
|
-
if (evidence !== 0)
|
|
524
|
-
return evidence;
|
|
525
|
-
return a.id.localeCompare(b.id);
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
function strongestEvidenceRank(evidence) {
|
|
529
|
-
if (evidence.some((item) => item.severity === 'error'))
|
|
530
|
-
return 0;
|
|
531
|
-
if (evidence.some((item) => item.severity === 'warning'))
|
|
532
|
-
return 1;
|
|
533
|
-
return 2;
|
|
534
|
-
}
|
|
535
|
-
function priorityRank(priority) {
|
|
536
|
-
if (priority === 'p0')
|
|
537
|
-
return 0;
|
|
538
|
-
if (priority === 'p1')
|
|
539
|
-
return 1;
|
|
540
|
-
return 2;
|
|
541
|
-
}
|
|
542
|
-
function reasonToEvidence(reason) {
|
|
543
|
-
return {
|
|
544
|
-
source: reason.source,
|
|
545
|
-
message: reason.message,
|
|
546
|
-
severity: reason.severity,
|
|
547
|
-
...(reason.file ? { file: reason.file } : {}),
|
|
548
|
-
...(reason.issueId ? { issueId: reason.issueId } : {}),
|
|
549
|
-
...(reason.tool ? { tool: reason.tool } : {}),
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
function filesFromReasons(reasons) {
|
|
553
|
-
return unique(reasons.map((reason) => reason.file).filter((file) => typeof file === 'string'));
|
|
554
|
-
}
|
|
555
|
-
function filesFromEvidence(evidence) {
|
|
556
|
-
return unique(evidence.map((item) => item.file).filter((file) => typeof file === 'string'));
|
|
557
|
-
}
|
|
558
|
-
function buildTopRisks(reasons, conflicts, extraRisks = []) {
|
|
559
|
-
const reasonRisks = reasons.map((reason) => ({
|
|
560
|
-
...reasonToEvidence(reason),
|
|
561
|
-
priority: reason.severity === 'error' ? 'p0' : 'p1',
|
|
562
|
-
}));
|
|
563
|
-
const conflictRisks = conflicts.map((conflict) => ({
|
|
564
|
-
source: 'coordination',
|
|
565
|
-
message: conflict.message,
|
|
566
|
-
severity: conflict.severity,
|
|
567
|
-
file: conflict.files[0],
|
|
568
|
-
priority: conflict.severity === 'error' ? 'p0' : 'p1',
|
|
569
|
-
}));
|
|
570
|
-
const seen = new Set();
|
|
571
|
-
return [...reasonRisks, ...conflictRisks, ...extraRisks]
|
|
572
|
-
.map((risk, index) => ({ risk, index }))
|
|
573
|
-
.filter((entry) => {
|
|
574
|
-
const { risk } = entry;
|
|
575
|
-
const key = `${risk.source}:${risk.file ?? ''}:${risk.message}`;
|
|
576
|
-
if (seen.has(key))
|
|
577
|
-
return false;
|
|
578
|
-
seen.add(key);
|
|
579
|
-
return true;
|
|
580
|
-
})
|
|
581
|
-
.sort((a, b) => {
|
|
582
|
-
const priority = priorityRank(a.risk.priority) - priorityRank(b.risk.priority);
|
|
583
|
-
if (priority !== 0)
|
|
584
|
-
return priority;
|
|
585
|
-
return a.index - b.index;
|
|
586
|
-
})
|
|
587
|
-
.map((entry) => entry.risk)
|
|
588
|
-
.slice(0, MAX_TOP_RISKS);
|
|
589
|
-
}
|
|
590
|
-
function annotateTasksWithOwners(tasks, ownership) {
|
|
591
|
-
if (!ownership)
|
|
592
|
-
return tasks;
|
|
593
|
-
return tasks.map((task) => {
|
|
594
|
-
const owner = ownerForTask(task, ownership);
|
|
595
|
-
if (!owner)
|
|
596
|
-
return task;
|
|
597
|
-
return {
|
|
598
|
-
...task,
|
|
599
|
-
owner,
|
|
600
|
-
handoffText: compact(`${task.handoffText} Owner: ${owner}.`, HANDOFF_LIMIT),
|
|
601
|
-
};
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
function annotateTopRisksWithOwners(risks, ownership) {
|
|
605
|
-
if (!ownership)
|
|
606
|
-
return risks;
|
|
607
|
-
return risks.map((risk) => {
|
|
608
|
-
const owner = ownerForFiles([risk.file].filter((file) => typeof file === 'string'), ownership);
|
|
609
|
-
return owner ? { ...risk, owner } : risk;
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
function ownerForTask(task, ownership) {
|
|
613
|
-
const evidenceFiles = task.evidence
|
|
614
|
-
.map((item) => item.file)
|
|
615
|
-
.filter((file) => typeof file === 'string' && file.length > 0);
|
|
616
|
-
return ownerForFiles([...task.files, ...evidenceFiles], ownership);
|
|
617
|
-
}
|
|
618
|
-
function ownerForFiles(files, ownership) {
|
|
619
|
-
for (const file of unique(files)) {
|
|
620
|
-
const owner = ownership(file);
|
|
621
|
-
if (owner)
|
|
622
|
-
return owner;
|
|
623
|
-
}
|
|
624
|
-
return undefined;
|
|
625
|
-
}
|
|
626
|
-
function summarizeWorkplan(mode, verdict, tasks, risks) {
|
|
627
|
-
if (tasks.length === 0)
|
|
628
|
-
return `${verdict}: ${mode} workplan has no recommended tasks`;
|
|
629
|
-
const riskText = risks.length > 0 ? `${risks.length} top risk(s)` : 'no top risks';
|
|
630
|
-
return `${verdict}: ${mode} workplan has ${tasks.length} task(s), starting with ${tasks[0]?.title}; ${riskText}`;
|
|
631
|
-
}
|
|
632
|
-
function taskToSuggestedActions(task) {
|
|
633
|
-
return task.suggestedTools.slice(0, 3).flatMap((tool) => {
|
|
634
|
-
const command = commandForSuggestedTool(tool, task);
|
|
635
|
-
if (!command)
|
|
636
|
-
return [];
|
|
637
|
-
return [
|
|
638
|
-
{
|
|
639
|
-
label: `Use ${tool} for ${task.title}`,
|
|
640
|
-
tool: tool.startsWith('projscan_') ? tool : undefined,
|
|
641
|
-
command,
|
|
642
|
-
},
|
|
643
|
-
];
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
function commandForSuggestedTool(tool, task) {
|
|
647
|
-
if (!tool.startsWith('projscan_'))
|
|
648
|
-
return task.verification.commands[0];
|
|
649
|
-
if (tool === 'projscan_file' && task.files[0]) {
|
|
650
|
-
return `projscan file "${escapeDoubleQuoted(task.files[0])}" --format json`;
|
|
651
|
-
}
|
|
652
|
-
const prefix = `projscan ${tool.slice('projscan_'.length).replace(/_/g, '-')}`;
|
|
653
|
-
return task.verification.commands.find((command) => command.startsWith(prefix));
|
|
654
|
-
}
|
|
655
|
-
function dedupeActions(actions) {
|
|
656
|
-
const seen = new Set();
|
|
657
|
-
const out = [];
|
|
658
|
-
for (const action of actions) {
|
|
659
|
-
const key = `${action.label}:${action.command ?? ''}:${action.tool ?? ''}`;
|
|
660
|
-
if (seen.has(key))
|
|
661
|
-
continue;
|
|
662
|
-
seen.add(key);
|
|
663
|
-
out.push(action);
|
|
664
|
-
}
|
|
665
|
-
return out.slice(0, 12);
|
|
666
|
-
}
|
|
98
|
+
export { rankWorkplanTasks };
|
|
667
99
|
function normalizeMaxTasks(value) {
|
|
668
100
|
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
669
101
|
return DEFAULT_MAX_TASKS;
|
|
@@ -672,10 +104,4 @@ function normalizeMaxTasks(value) {
|
|
|
672
104
|
function unique(values) {
|
|
673
105
|
return [...new Set(values)];
|
|
674
106
|
}
|
|
675
|
-
function compact(value, maxLength) {
|
|
676
|
-
const oneLine = value.replace(/\s+/g, ' ').trim();
|
|
677
|
-
if (oneLine.length <= maxLength)
|
|
678
|
-
return oneLine;
|
|
679
|
-
return `${oneLine.slice(0, maxLength - 3).trimEnd()}...`;
|
|
680
|
-
}
|
|
681
107
|
//# sourceMappingURL=workplan.js.map
|