nexus-prime 7.9.7 → 7.9.9
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/dist/agents/adapters/mcp/definitions.js +13 -0
- package/dist/agents/adapters/mcp/handlers/orchestration.d.ts +2 -0
- package/dist/agents/adapters/mcp/handlers/orchestration.js +34 -5
- package/dist/agents/adapters/mcp/handlers/runtime.js +36 -1
- package/dist/agents/adapters/mcp/helpers.js +1 -1
- package/dist/dashboard/routes/runtime.js +9 -0
- package/dist/dashboard/selectors/assets-selector.js +2 -0
- package/dist/dashboard/types.d.ts +1 -0
- package/dist/engines/orchestrator/scoring.js +3 -0
- package/dist/engines/orchestrator.js +57 -12
- package/dist/engines/pattern-registry.d.ts +12 -0
- package/dist/engines/pattern-registry.js +65 -0
- package/dist/engines/runtime-assets.js +3 -0
- package/dist/engines/specialist-roster.js +16 -8
- package/dist/engines/user-workflow-trace.d.ts +47 -0
- package/dist/engines/user-workflow-trace.js +281 -0
- package/dist/phantom/runtime/worktree.d.ts +6 -1
- package/dist/phantom/runtime/worktree.js +27 -1
- package/dist/phantom/runtime.d.ts +6 -0
- package/dist/phantom/runtime.js +96 -14
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ export const FIRST_CLASS_TOOLS = new Set([
|
|
|
15
15
|
'nexus_search',
|
|
16
16
|
'nexus_runtime_health',
|
|
17
17
|
'nexus_run_status',
|
|
18
|
+
'nexus_user_workflow_trace',
|
|
18
19
|
'kernel_run_step',
|
|
19
20
|
'kernel_run_verify',
|
|
20
21
|
'kernel_run_merge',
|
|
@@ -881,6 +882,18 @@ export function buildMcpToolDefinitions() {
|
|
|
881
882
|
required: ['runId'],
|
|
882
883
|
},
|
|
883
884
|
},
|
|
885
|
+
{
|
|
886
|
+
name: 'nexus_user_workflow_trace',
|
|
887
|
+
description: 'Return the local user-agent workflow trace for a run: user goal, agent actions, context, verification, failure signals, and next actions.',
|
|
888
|
+
inputSchema: {
|
|
889
|
+
type: 'object',
|
|
890
|
+
properties: {
|
|
891
|
+
runId: { type: 'string', description: 'Execution run ID' },
|
|
892
|
+
format: { type: 'string', enum: ['text', 'json'], description: 'Response format' },
|
|
893
|
+
},
|
|
894
|
+
required: ['runId'],
|
|
895
|
+
},
|
|
896
|
+
},
|
|
884
897
|
// ── Darwin Loop ──────────────────────────────────────────────────
|
|
885
898
|
{
|
|
886
899
|
name: 'nexus_darwin_propose',
|
|
@@ -10,5 +10,7 @@ type McpResult = {
|
|
|
10
10
|
text: string;
|
|
11
11
|
}>;
|
|
12
12
|
};
|
|
13
|
+
export declare function extractSkillSelectorsFromPrompt(prompt: string): string[];
|
|
14
|
+
export declare function inferSpawnWorkersIntent(actions: unknown[]): 'plan' | 'mutate';
|
|
13
15
|
export declare function handleOrchestrationGroup(toolName: string, hctx: McpHandlerCtx, request: any, args: Record<string, unknown>, ctx?: any): Promise<McpResult | undefined>;
|
|
14
16
|
export {};
|
|
@@ -20,6 +20,25 @@ import { getSharedTelemetry } from '../../../../engines/telemetry-remote.js';
|
|
|
20
20
|
import { requireRuntime } from '../util/require-runtime.js';
|
|
21
21
|
import { ensureCrGraphBuilt } from '../../../../engines/code-review-graph-client.js';
|
|
22
22
|
import { recordFirstBootstrap } from '../../../../engines/telemetry.js';
|
|
23
|
+
export function extractSkillSelectorsFromPrompt(prompt) {
|
|
24
|
+
const selectors = new Set();
|
|
25
|
+
const add = (value) => {
|
|
26
|
+
const cleaned = value?.trim().replace(/^[$@]+/, '');
|
|
27
|
+
if (cleaned && /^[a-z0-9][a-z0-9._-]{1,120}$/i.test(cleaned)) {
|
|
28
|
+
selectors.add(cleaned);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
for (const match of prompt.matchAll(/\[[$@]?([a-z0-9][a-z0-9._-]{1,120})\]\([^)]*\/\.agents?\/skills\/[^)]*\/SKILL\.md\)/gi)) {
|
|
32
|
+
add(match[1]);
|
|
33
|
+
}
|
|
34
|
+
for (const match of prompt.matchAll(/\/\.agents?\/skills\/([^/\s)]+)\/SKILL\.md/gi)) {
|
|
35
|
+
add(match[1]);
|
|
36
|
+
}
|
|
37
|
+
return [...selectors];
|
|
38
|
+
}
|
|
39
|
+
export function inferSpawnWorkersIntent(actions) {
|
|
40
|
+
return actions.length > 0 ? 'mutate' : 'plan';
|
|
41
|
+
}
|
|
23
42
|
export async function handleOrchestrationGroup(toolName, hctx, request, args, ctx) {
|
|
24
43
|
const runtimeError = requireRuntime(hctx);
|
|
25
44
|
if (runtimeError)
|
|
@@ -315,9 +334,11 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
315
334
|
const workers = request.params.arguments?.workers == null
|
|
316
335
|
? undefined
|
|
317
336
|
: Number(request.params.arguments.workers);
|
|
318
|
-
const
|
|
337
|
+
const explicitSkills = Array.isArray(request.params.arguments?.skills)
|
|
319
338
|
? request.params.arguments.skills.map(String)
|
|
320
|
-
:
|
|
339
|
+
: [];
|
|
340
|
+
const promptSkills = extractSkillSelectorsFromPrompt(prompt);
|
|
341
|
+
const skills = [...new Set([...explicitSkills, ...promptSkills])];
|
|
321
342
|
const workflows = Array.isArray(request.params.arguments?.workflows)
|
|
322
343
|
? request.params.arguments.workflows.map(String)
|
|
323
344
|
: undefined;
|
|
@@ -374,7 +395,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
374
395
|
const execution = await hctx.nexusRef.orchestrate(prompt, applyExecutionPreset({
|
|
375
396
|
files,
|
|
376
397
|
workers,
|
|
377
|
-
skillNames: skills,
|
|
398
|
+
skillNames: skills.length > 0 ? skills : undefined,
|
|
378
399
|
workflowSelectors: workflows,
|
|
379
400
|
hookSelectors: hooks,
|
|
380
401
|
automationSelectors: automations,
|
|
@@ -769,6 +790,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
769
790
|
? String(request.params.arguments.executionPreset)
|
|
770
791
|
: undefined;
|
|
771
792
|
const preset = resolveExecutionPreset(executionPreset);
|
|
793
|
+
const inferredIntent = inferSpawnWorkersIntent(actions);
|
|
772
794
|
// Surface worker-plan intent on the SSE feed without persisting it as a memory.
|
|
773
795
|
try {
|
|
774
796
|
nexusEventBus.emit('memory.snapshot', {
|
|
@@ -796,11 +818,18 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
796
818
|
backendSelectors: { memoryBackend, compressionBackend, dslCompiler },
|
|
797
819
|
backendMode,
|
|
798
820
|
optimizationProfile,
|
|
821
|
+
intent: inferredIntent,
|
|
799
822
|
executionMode: 'manual-low-level',
|
|
800
823
|
manualOverrides: ['nexus_spawn_workers'],
|
|
801
824
|
}, executionPreset));
|
|
802
825
|
const verifiedWorkers = execution.workerResults.filter(result => result.verified).length;
|
|
803
826
|
const modifiedFiles = execution.workerResults.reduce((sum, result) => sum + result.modifiedFiles.length, 0);
|
|
827
|
+
const noDiffInspected = execution.state === 'inspected'
|
|
828
|
+
&& modifiedFiles === 0
|
|
829
|
+
&& !execution.workerResults.some((result) => result.diff?.trim());
|
|
830
|
+
const displayedDecision = noDiffInspected
|
|
831
|
+
? 'no-applicable-diff'
|
|
832
|
+
: execution.finalDecision?.action ?? 'none';
|
|
804
833
|
execution.activeSkills.forEach(skill => {
|
|
805
834
|
hctx.sessionDNA.recordSkill(skill.name);
|
|
806
835
|
if (skill.scope === 'global' || skill.rolloutStatus === 'promoted') {
|
|
@@ -826,7 +855,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
826
855
|
`Run: ${execution.runId.padEnd(28, ' ')} State: ${execution.state.padEnd(18, ' ')}`,
|
|
827
856
|
`Workers: ${execution.workerResults.length.toString().padEnd(5, ' ')} Verified: ${verifiedWorkers.toString().padEnd(10, ' ')} Files: ${String(modifiedFiles).padEnd(12, ' ')}`,
|
|
828
857
|
`${`Preset: ${preset?.name ?? 'manual'}`.padEnd(61, ' ')}`,
|
|
829
|
-
`Decision: ${
|
|
858
|
+
`Decision: ${displayedDecision.padEnd(52, ' ')}`
|
|
830
859
|
], execution.state === 'merged' || execution.state === 'inspected' ? '32' : execution.state === 'rolled_back' ? '33' : '31');
|
|
831
860
|
return {
|
|
832
861
|
content: [{
|
|
@@ -841,7 +870,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
841
870
|
preset ? `Preset: ${preset.name} (${preset.id})` : null,
|
|
842
871
|
`Verified Workers: ${verifiedWorkers}`,
|
|
843
872
|
`Modified Files: ${modifiedFiles}`,
|
|
844
|
-
`Decision: ${
|
|
873
|
+
`Decision: ${displayedDecision}`,
|
|
845
874
|
`Recommended Strategy: ${execution.finalDecision?.recommendedStrategy ?? 'n/a'}`,
|
|
846
875
|
`Planner: ${execution.plannerResult?.summary ?? 'n/a'}`,
|
|
847
876
|
`Crew: ${execution.plannerResult?.selectedCrew?.name ?? 'n/a'}`,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* federation_status, run_status.
|
|
5
5
|
* Extracted from mcp.ts (Phase 3 split).
|
|
6
6
|
*/
|
|
7
|
-
import { formatBullets, formatJsonDetails, buildAutoMemorySummary, } from '../helpers.js';
|
|
7
|
+
import { formatBullets, formatJsonDetails, truncateText, buildAutoMemorySummary, } from '../helpers.js';
|
|
8
8
|
import { nexusEventBus } from '../../../../engines/event-bus.js';
|
|
9
9
|
import { getSharedLicenseManager, snapshotPCU, formatPCUStatus } from '../../../../licensing/index.js';
|
|
10
10
|
import { TokenAnalyticsEngine } from '../../../../engines/token-analytics.js';
|
|
@@ -13,6 +13,7 @@ import { requireRuntime } from '../util/require-runtime.js';
|
|
|
13
13
|
import { getAsyncGate } from '../async-gate.js';
|
|
14
14
|
import { getRun as getOrchRun } from '../../../../engines/orchestrator/store.js';
|
|
15
15
|
import { nxlResult } from '../../../../nxl/index.js';
|
|
16
|
+
import { summarizeUserWorkflowTrace } from '../../../../engines/user-workflow-trace.js';
|
|
16
17
|
export async function handleRuntimeGroup(toolName, hctx, request, args, ctx) {
|
|
17
18
|
const runtimeError = requireRuntime(hctx);
|
|
18
19
|
if (runtimeError)
|
|
@@ -226,6 +227,40 @@ export async function handleRuntimeGroup(toolName, hctx, request, args, ctx) {
|
|
|
226
227
|
}],
|
|
227
228
|
};
|
|
228
229
|
}
|
|
230
|
+
case 'nexus_user_workflow_trace': {
|
|
231
|
+
const runId = String(request.params.arguments?.runId ?? '');
|
|
232
|
+
const fmt = String(request.params.arguments?.format ?? 'text');
|
|
233
|
+
const trace = await hctx.getRuntime().getUserWorkflowTrace?.(runId);
|
|
234
|
+
if (!trace) {
|
|
235
|
+
return { content: [{ type: 'text', text: `User workflow trace not found: ${runId}` }] };
|
|
236
|
+
}
|
|
237
|
+
if (fmt === 'json') {
|
|
238
|
+
return { content: [{ type: 'text', text: JSON.stringify(trace, null, 2) }] };
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
content: [{
|
|
242
|
+
type: 'text',
|
|
243
|
+
text: [
|
|
244
|
+
summarizeUserWorkflowTrace(trace),
|
|
245
|
+
'',
|
|
246
|
+
formatBullets([
|
|
247
|
+
`Run: ${trace.runId}`,
|
|
248
|
+
`Goal: ${truncateText(trace.goal, 120)}`,
|
|
249
|
+
`Actions: ${trace.userJourney.actions.length ? trace.userJourney.actions.slice(0, 5).join(' | ') : 'none recorded'}`,
|
|
250
|
+
`Files: ${trace.userJourney.selectedFiles.length ? trace.userJourney.selectedFiles.slice(0, 5).join(', ') : 'none selected'}`,
|
|
251
|
+
`Agents: ${trace.userJourney.selectedSpecialists.length ? trace.userJourney.selectedSpecialists.slice(0, 5).join(', ') : 'none selected'}`,
|
|
252
|
+
`Signals: ${trace.signals.length ? trace.signals.map(signal => `${signal.severity}:${signal.code}`).slice(0, 8).join(', ') : 'none'}`,
|
|
253
|
+
]),
|
|
254
|
+
trace.signals.length
|
|
255
|
+
? `Failure signals\n${formatBullets(trace.signals.map(signal => `${signal.code}: ${signal.summary}`))}`
|
|
256
|
+
: '',
|
|
257
|
+
trace.nextActions.length
|
|
258
|
+
? `Next actions\n${formatBullets(trace.nextActions)}`
|
|
259
|
+
: '',
|
|
260
|
+
].filter(Boolean).join('\n\n'),
|
|
261
|
+
}],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
229
264
|
case 'nexus_run_status': {
|
|
230
265
|
const runId = String(request.params.arguments?.runId ?? '');
|
|
231
266
|
const fmt = String(request.params.arguments?.format ?? 'text');
|
|
@@ -190,7 +190,7 @@ export function isReadOnlyMcpTool(toolName) {
|
|
|
190
190
|
'nexus_list_', 'nexus_describe_', 'nexus_runtime_health',
|
|
191
191
|
'nexus_doctor', 'nexus_token_report', 'nexus_recall_memory',
|
|
192
192
|
'nexus_temporal_query', 'nexus_memory_stats', 'nexus_federation_status',
|
|
193
|
-
'nexus_license_usage', 'nexus_run_status', 'nexus_selection_ledger',
|
|
193
|
+
'nexus_license_usage', 'nexus_run_status', 'nexus_user_workflow_trace', 'nexus_selection_ledger',
|
|
194
194
|
'nexus_operative_journal',
|
|
195
195
|
];
|
|
196
196
|
return skipPrefixes.some(p => toolName.startsWith(p));
|
|
@@ -136,6 +136,15 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
|
|
|
136
136
|
ctx.respondJson(res, entry ?? { error: 'run-token-telemetry-not-found', runId }, entry ? 200 : 404);
|
|
137
137
|
return true;
|
|
138
138
|
}
|
|
139
|
+
const userWorkflowMatch = req.method === 'GET'
|
|
140
|
+
? /^\/api\/runs\/(.+)\/user-workflow$/.exec(url.pathname)
|
|
141
|
+
: null;
|
|
142
|
+
if (userWorkflowMatch) {
|
|
143
|
+
const runId = decodeURIComponent(userWorkflowMatch[1]);
|
|
144
|
+
const trace = await ctx.getRuntime()?.getUserWorkflowTrace?.(runId);
|
|
145
|
+
ctx.respondJson(res, trace ?? { error: 'user-workflow-trace-not-found', runId }, trace ? 200 : 404);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
139
148
|
if (req.method === 'GET' && url.pathname.startsWith('/api/runs/')) {
|
|
140
149
|
const runId = decodeURIComponent(url.pathname.replace('/api/runs/', ''));
|
|
141
150
|
const run = await ctx.getRuntime()?.getRun?.(runId);
|
|
@@ -23,11 +23,13 @@ export function serializeSpecialistsCatalog(specialists) {
|
|
|
23
23
|
export async function buildAssetsSurface(ctx, url) {
|
|
24
24
|
const state = collectDashboardBaseState(ctx, url);
|
|
25
25
|
const runtimeId = state.runtime?.getRuntimeId?.() ?? 'default';
|
|
26
|
+
const patterns = ctx.getOrchestrator?.()?.listPatterns?.() ?? [];
|
|
26
27
|
return {
|
|
27
28
|
repoIdentity: state.snapshot?.repoIdentity ?? ctx.getSelectedRepoIdentity(url),
|
|
28
29
|
usage: state.usage,
|
|
29
30
|
selectionAudit: serializeSelectionAudit(state.usage),
|
|
30
31
|
featureRegistry: await ctx.getCachedValue('feature-registry', 30_000, () => Promise.resolve(buildFeatureRegistry(ctx.repoRoot))),
|
|
32
|
+
patternPacks: patterns.filter((pattern) => Array.isArray(pattern.tags) && pattern.tags.includes('pattern-pack')),
|
|
31
33
|
skills: state.runtime?.listSkills?.() ?? [],
|
|
32
34
|
specialists: await ctx.getCachedValue(`catalog:specialists:${runtimeId}`, 15_000, () => Promise.resolve(serializeSpecialistsCatalog(state.runtime?.listSpecialists?.() ?? []))),
|
|
33
35
|
crews: state.runtime?.listCrews?.() ?? [],
|
|
@@ -293,6 +293,9 @@ export function decideWorkers(requestedWorkers, plannedWorkers, phaseCount, inte
|
|
|
293
293
|
if (typeof requestedWorkers === 'number' && requestedWorkers > 0) {
|
|
294
294
|
return Math.max(1, Math.min(7, requestedWorkers));
|
|
295
295
|
}
|
|
296
|
+
if (intent.taskType === 'release') {
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
296
299
|
if (intent.riskClass !== 'high'
|
|
297
300
|
&& intent.complexity <= 3
|
|
298
301
|
&& phaseCount <= 2
|
|
@@ -2299,7 +2299,7 @@ export class OrchestratorEngine {
|
|
|
2299
2299
|
elapsedMs: Date.now() - funnelStart,
|
|
2300
2300
|
});
|
|
2301
2301
|
// ── Funnel Stage 2: vote ─────────────────────────────────────────────────
|
|
2302
|
-
const skillSelection = this.resolveCatalogVotes('skill', task, intent, skillItems, {
|
|
2302
|
+
const skillSelection = this.resolveCatalogVotes('skill', task, intent, options.skillNames?.length ? allSkillItems : skillItems, {
|
|
2303
2303
|
explicit: options.skillNames,
|
|
2304
2304
|
planner: planner.selectedSkills,
|
|
2305
2305
|
knowledge: knowledgeFabric.recommendations.skills,
|
|
@@ -2308,7 +2308,7 @@ export class OrchestratorEngine {
|
|
|
2308
2308
|
limit: 5,
|
|
2309
2309
|
selector: 'name',
|
|
2310
2310
|
});
|
|
2311
|
-
const workflowSelection = this.resolveCatalogVotes('workflow', task, intent, workflowItems, {
|
|
2311
|
+
const workflowSelection = this.resolveCatalogVotes('workflow', task, intent, options.workflowSelectors?.length ? allWorkflowItems : workflowItems, {
|
|
2312
2312
|
explicit: options.workflowSelectors,
|
|
2313
2313
|
planner: planner.selectedWorkflows,
|
|
2314
2314
|
knowledge: knowledgeFabric.recommendations.workflows,
|
|
@@ -2316,7 +2316,24 @@ export class OrchestratorEngine {
|
|
|
2316
2316
|
limit: 4,
|
|
2317
2317
|
selector: 'name',
|
|
2318
2318
|
});
|
|
2319
|
-
const
|
|
2319
|
+
const selectedWorkflowValues = intent.taskType === 'release'
|
|
2320
|
+
? dedupeStrings(['release-pipeline', ...workflowSelection.selectedValues])
|
|
2321
|
+
: workflowSelection.selectedValues;
|
|
2322
|
+
const workflowSelectedEntries = [
|
|
2323
|
+
...workflowSelection.selectedEntries,
|
|
2324
|
+
...selectedWorkflowValues
|
|
2325
|
+
.filter((value) => !workflowSelection.selectedEntries.some((entry) => entry.name === value || entry.id === value))
|
|
2326
|
+
.map((value) => this.toArtifactAuditEntry('workflow', value, workflowItems, {
|
|
2327
|
+
score: 1,
|
|
2328
|
+
source: 'planner',
|
|
2329
|
+
confidence: 'high',
|
|
2330
|
+
reason: 'Release intent requires the release-pipeline workflow so package, audit, and smoke evidence is collected.',
|
|
2331
|
+
selector: 'name',
|
|
2332
|
+
selectionPolicy: 'global-first',
|
|
2333
|
+
authorityTier: this.getAuthorityTier('workflow'),
|
|
2334
|
+
})),
|
|
2335
|
+
];
|
|
2336
|
+
const hookSelection = this.resolveCatalogVotes('hook', task, intent, options.hookSelectors?.length ? allHookItems : hookItems, {
|
|
2320
2337
|
explicit: options.hookSelectors,
|
|
2321
2338
|
planner: [],
|
|
2322
2339
|
knowledge: knowledgeFabric.recommendations.hooks,
|
|
@@ -2324,7 +2341,7 @@ export class OrchestratorEngine {
|
|
|
2324
2341
|
limit: intent.riskClass === 'high' ? 3 : 2,
|
|
2325
2342
|
selector: 'name',
|
|
2326
2343
|
});
|
|
2327
|
-
const automationSelection = this.resolveCatalogVotes('automation', task, intent, automationItems, {
|
|
2344
|
+
const automationSelection = this.resolveCatalogVotes('automation', task, intent, options.automationSelectors?.length ? allAutomationItems : automationItems, {
|
|
2328
2345
|
explicit: options.automationSelectors,
|
|
2329
2346
|
planner: [],
|
|
2330
2347
|
knowledge: knowledgeFabric.recommendations.automations,
|
|
@@ -2332,7 +2349,7 @@ export class OrchestratorEngine {
|
|
|
2332
2349
|
limit: intent.taskType === 'release' || this.sessionState.repeatedFailures > 0 ? 3 : 2,
|
|
2333
2350
|
selector: 'name',
|
|
2334
2351
|
});
|
|
2335
|
-
const specialistSelection = this.resolveCatalogVotes('specialist', task, intent, specialistItems, {
|
|
2352
|
+
const specialistSelection = this.resolveCatalogVotes('specialist', task, intent, options.specialistSelectors?.length ? allSpecialistItems : specialistItems, {
|
|
2336
2353
|
explicit: options.specialistSelectors,
|
|
2337
2354
|
planner: planner.selectedSpecialists.map((specialist) => specialist.specialistId),
|
|
2338
2355
|
knowledge: knowledgeFabric.recommendations.specialists,
|
|
@@ -2340,7 +2357,7 @@ export class OrchestratorEngine {
|
|
|
2340
2357
|
limit: 4,
|
|
2341
2358
|
selector: 'id',
|
|
2342
2359
|
});
|
|
2343
|
-
const crewSelection = this.resolveCatalogVotes('crew', task, intent, crewItems, {
|
|
2360
|
+
const crewSelection = this.resolveCatalogVotes('crew', task, intent, options.crewSelectors?.length ? allCrewItems : crewItems, {
|
|
2344
2361
|
explicit: options.crewSelectors,
|
|
2345
2362
|
planner: planner.selectedCrew ? [planner.selectedCrew.crewId] : [],
|
|
2346
2363
|
knowledge: knowledgeFabric.recommendations.crews,
|
|
@@ -2356,7 +2373,7 @@ export class OrchestratorEngine {
|
|
|
2356
2373
|
selectionSummary: {
|
|
2357
2374
|
specialists: specialistSelection.selectedValues.length,
|
|
2358
2375
|
skills: skillSelection.selectedValues.length,
|
|
2359
|
-
workflows:
|
|
2376
|
+
workflows: selectedWorkflowValues.length,
|
|
2360
2377
|
hooks: hookSelection.selectedValues.length,
|
|
2361
2378
|
automations: automationSelection.selectedValues.length,
|
|
2362
2379
|
crews: crewSelection.selectedValues.length,
|
|
@@ -2367,7 +2384,7 @@ export class OrchestratorEngine {
|
|
|
2367
2384
|
...crewSelection.selectedEntries,
|
|
2368
2385
|
...specialistSelection.selectedEntries,
|
|
2369
2386
|
...skillSelection.selectedEntries,
|
|
2370
|
-
...
|
|
2387
|
+
...workflowSelectedEntries,
|
|
2371
2388
|
...hookSelection.selectedEntries,
|
|
2372
2389
|
...automationSelection.selectedEntries,
|
|
2373
2390
|
];
|
|
@@ -2388,7 +2405,7 @@ export class OrchestratorEngine {
|
|
|
2388
2405
|
selectionSummary: {
|
|
2389
2406
|
specialists: specialistSelection.selectedValues.length,
|
|
2390
2407
|
skills: skillSelection.selectedValues.length,
|
|
2391
|
-
workflows:
|
|
2408
|
+
workflows: selectedWorkflowValues.length,
|
|
2392
2409
|
hooks: hookSelection.selectedValues.length,
|
|
2393
2410
|
automations: automationSelection.selectedValues.length,
|
|
2394
2411
|
crews: crewSelection.selectedValues.length,
|
|
@@ -2400,7 +2417,7 @@ export class OrchestratorEngine {
|
|
|
2400
2417
|
crew: crewSelection.selectedValues[0],
|
|
2401
2418
|
specialists: specialistSelection.selectedValues,
|
|
2402
2419
|
skills: skillSelection.selectedValues,
|
|
2403
|
-
workflows:
|
|
2420
|
+
workflows: selectedWorkflowValues,
|
|
2404
2421
|
hooks: hookSelection.selectedValues,
|
|
2405
2422
|
automations: automationSelection.selectedValues,
|
|
2406
2423
|
audit: {
|
|
@@ -2492,7 +2509,19 @@ export class OrchestratorEngine {
|
|
|
2492
2509
|
resolveCatalogVotes(kind, task, intent, items, input) {
|
|
2493
2510
|
const explicit = dedupeStrings(input.explicit ?? []);
|
|
2494
2511
|
if (explicit.length > 0) {
|
|
2495
|
-
const
|
|
2512
|
+
const resolved = explicit.map((value) => {
|
|
2513
|
+
const item = items.find((entry) => entry.id === value || entry.name === value);
|
|
2514
|
+
return {
|
|
2515
|
+
value,
|
|
2516
|
+
item,
|
|
2517
|
+
selectedValue: input.selector === 'id'
|
|
2518
|
+
? (item?.id ?? value)
|
|
2519
|
+
: (item?.name ?? value),
|
|
2520
|
+
};
|
|
2521
|
+
});
|
|
2522
|
+
const selectedEntries = resolved
|
|
2523
|
+
.filter((entry) => entry.item)
|
|
2524
|
+
.map((entry) => this.toArtifactAuditEntry(kind, entry.selectedValue, items, {
|
|
2496
2525
|
score: 1,
|
|
2497
2526
|
source: 'explicit',
|
|
2498
2527
|
confidence: 'high',
|
|
@@ -2500,7 +2529,23 @@ export class OrchestratorEngine {
|
|
|
2500
2529
|
selector: input.selector,
|
|
2501
2530
|
selectionPolicy: 'explicit-only',
|
|
2502
2531
|
}));
|
|
2503
|
-
|
|
2532
|
+
const rejectedEntries = resolved
|
|
2533
|
+
.filter((entry) => !entry.item)
|
|
2534
|
+
.map((entry) => this.toArtifactAuditEntry(kind, entry.value, items, {
|
|
2535
|
+
score: 1,
|
|
2536
|
+
source: 'explicit',
|
|
2537
|
+
confidence: 'high',
|
|
2538
|
+
reason: 'User provided a hard constraint, but the selector was not found in the runtime catalog.',
|
|
2539
|
+
selector: input.selector,
|
|
2540
|
+
selectionPolicy: 'explicit-only',
|
|
2541
|
+
blockedReason: 'Explicit selector is not present in the runtime catalog.',
|
|
2542
|
+
authorityTier: this.getAuthorityTier(kind),
|
|
2543
|
+
}, false));
|
|
2544
|
+
return {
|
|
2545
|
+
selectedValues: selectedEntries.map((entry) => input.selector === 'id' ? entry.id : entry.name),
|
|
2546
|
+
selectedEntries,
|
|
2547
|
+
rejectedEntries,
|
|
2548
|
+
};
|
|
2504
2549
|
}
|
|
2505
2550
|
const votes = new Map();
|
|
2506
2551
|
const applyVote = (value, source, score, confidence, reason) => {
|
|
@@ -16,6 +16,18 @@ export interface PatternCard {
|
|
|
16
16
|
successCount: number;
|
|
17
17
|
failureCount: number;
|
|
18
18
|
lastUsedAt?: number;
|
|
19
|
+
patternPack?: {
|
|
20
|
+
kind: 'flow-kit';
|
|
21
|
+
flows: string[];
|
|
22
|
+
starterFiles: Array<{
|
|
23
|
+
path: string;
|
|
24
|
+
purpose: string;
|
|
25
|
+
}>;
|
|
26
|
+
memorySeeds: string[];
|
|
27
|
+
lifecycle: string[];
|
|
28
|
+
acceptanceChecks: string[];
|
|
29
|
+
antiPatterns: string[];
|
|
30
|
+
};
|
|
19
31
|
}
|
|
20
32
|
export interface PatternSearchResult extends PatternCard {
|
|
21
33
|
score: number;
|
|
@@ -93,6 +93,71 @@ const BUILTIN_PATTERNS = [
|
|
|
93
93
|
successCount: 4,
|
|
94
94
|
failureCount: 0,
|
|
95
95
|
},
|
|
96
|
+
{
|
|
97
|
+
patternId: 'pattern_pack_auth_signup_flow',
|
|
98
|
+
name: 'Auth and Signup Flow Pack',
|
|
99
|
+
category: 'workflow',
|
|
100
|
+
summary: 'Ground common auth, signup, onboarding, and session flows in reusable security-aware defaults without generating stale boilerplate.',
|
|
101
|
+
instructions: 'Use this pack when a task mentions auth, login, signup, registration, onboarding, sessions, passwords, magic links, OAuth, or account creation. Start from the lifecycle and acceptance checks, inspect the target framework, then generate only framework-appropriate code with tests and security review. Never paste secrets, never invent provider config, and never bypass existing auth libraries.',
|
|
102
|
+
tags: ['pattern-pack', 'auth', 'signup', 'registration', 'login', 'onboarding', 'security', 'session'],
|
|
103
|
+
stages: ['bootstrap', 'planning', 'mutation', 'verification'],
|
|
104
|
+
suggestedSkills: ['broken-authentication', 'backend-dev-guidelines', 'frontend-dev-guidelines', 'security-auditor'],
|
|
105
|
+
suggestedWorkflows: ['backend-execution-loop', 'frontend-execution-loop'],
|
|
106
|
+
suggestedHooks: ['before-verify-approval'],
|
|
107
|
+
suggestedAutomations: [],
|
|
108
|
+
suggestedCrews: ['crew_implementation'],
|
|
109
|
+
suggestedSpecialists: [
|
|
110
|
+
'specialist_engineering-engineering-backend-architect',
|
|
111
|
+
'specialist_engineering-engineering-frontend-developer',
|
|
112
|
+
'specialist_engineering-engineering-security-engineer',
|
|
113
|
+
],
|
|
114
|
+
confidence: 0.81,
|
|
115
|
+
successCount: 0,
|
|
116
|
+
failureCount: 0,
|
|
117
|
+
patternPack: {
|
|
118
|
+
kind: 'flow-kit',
|
|
119
|
+
flows: [
|
|
120
|
+
'email/password signup',
|
|
121
|
+
'login/logout',
|
|
122
|
+
'session refresh',
|
|
123
|
+
'password reset',
|
|
124
|
+
'email verification',
|
|
125
|
+
'OAuth handoff',
|
|
126
|
+
'first-run onboarding',
|
|
127
|
+
],
|
|
128
|
+
starterFiles: [
|
|
129
|
+
{ path: 'auth/provider', purpose: 'Wrap the project auth provider or framework-native session adapter.' },
|
|
130
|
+
{ path: 'auth/routes', purpose: 'Handle signup, login, logout, callback, and reset endpoints.' },
|
|
131
|
+
{ path: 'auth/ui', purpose: 'Expose signup, login, reset, and verification states.' },
|
|
132
|
+
{ path: 'auth/tests', purpose: 'Cover happy path, invalid credentials, session expiry, and access-control regressions.' },
|
|
133
|
+
],
|
|
134
|
+
memorySeeds: [
|
|
135
|
+
'Auth work must reuse project-native libraries and existing session boundaries before adding new dependencies.',
|
|
136
|
+
'Signup flows need explicit abuse, rate-limit, email verification, and password-reset checks.',
|
|
137
|
+
'Never store provider secrets, reset tokens, or session material in Nexus memory.',
|
|
138
|
+
],
|
|
139
|
+
lifecycle: [
|
|
140
|
+
'Detect framework and existing auth surface.',
|
|
141
|
+
'Select provider pattern and security constraints.',
|
|
142
|
+
'Patch smallest route, UI, and persistence surface.',
|
|
143
|
+
'Verify with unit, integration, and access-control checks.',
|
|
144
|
+
'Store only sanitized implementation decisions as memory.',
|
|
145
|
+
],
|
|
146
|
+
acceptanceChecks: [
|
|
147
|
+
'Unauthenticated users cannot access protected routes.',
|
|
148
|
+
'Signup and login errors are explicit without leaking account existence.',
|
|
149
|
+
'Sessions expire or refresh according to project policy.',
|
|
150
|
+
'Password reset and verification tokens are one-time and scoped.',
|
|
151
|
+
'Tests cover success, failure, and privilege-boundary cases.',
|
|
152
|
+
],
|
|
153
|
+
antiPatterns: [
|
|
154
|
+
'Rolling custom crypto.',
|
|
155
|
+
'Saving secrets or tokens into memory.',
|
|
156
|
+
'Adding auth dependencies before checking existing stack.',
|
|
157
|
+
'Generating UI-only signup without backend/session proof.',
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
96
161
|
];
|
|
97
162
|
export class PatternRegistry {
|
|
98
163
|
statePath;
|
|
@@ -595,6 +595,9 @@ const SPECIALIZED_WORKFLOW_PACKS = [
|
|
|
595
595
|
title: 'Collect package, audit, and smoke evidence',
|
|
596
596
|
checkpoint: 'before-verify',
|
|
597
597
|
role: 'verifier',
|
|
598
|
+
bindings: [
|
|
599
|
+
{ type: 'run_command', command: 'npm run qa:release' },
|
|
600
|
+
],
|
|
598
601
|
},
|
|
599
602
|
{
|
|
600
603
|
title: 'Record publish readiness and remaining blockers',
|
|
@@ -41,25 +41,27 @@ export function getCrewTemplate(crewId) {
|
|
|
41
41
|
export function planSpecialists(input) {
|
|
42
42
|
const requestedCrews = input.requestedCrews ?? [];
|
|
43
43
|
const requestedSpecialists = input.requestedSpecialists ?? [];
|
|
44
|
+
const requestedSkills = input.requestedSkills ?? [];
|
|
45
|
+
const requestedWorkflows = input.requestedWorkflows ?? [];
|
|
44
46
|
const goal = input.goal;
|
|
45
47
|
const matchedDomains = detectDomains(goal, [
|
|
46
48
|
...(input.files ?? []),
|
|
47
49
|
...requestedCrews,
|
|
48
50
|
...requestedSpecialists,
|
|
49
|
-
...
|
|
50
|
-
...
|
|
51
|
+
...requestedSkills,
|
|
52
|
+
...requestedWorkflows,
|
|
51
53
|
]);
|
|
52
54
|
const taskSignals = analyzeTaskContext(goal, matchedDomains, input.files ?? []);
|
|
53
55
|
const selectedCrew = selectCrew(goal, matchedDomains, requestedCrews);
|
|
54
56
|
const selectedSpecialists = rankSpecialists(goal, matchedDomains, selectedCrew, requestedSpecialists, input.optimizationProfile ?? 'standard');
|
|
55
|
-
const selectedSkills = rankAssetSelectors(goal, matchedDomains, input.files ?? [], dedupeStrings([
|
|
56
|
-
...
|
|
57
|
+
const selectedSkills = pinRequestedSelectors(requestedSkills, rankAssetSelectors(goal, matchedDomains, input.files ?? [], dedupeStrings([
|
|
58
|
+
...requestedSkills,
|
|
57
59
|
...selectedSpecialists.flatMap((entry) => getSpecialist(entry.specialistId)?.recommendedSkills ?? []),
|
|
58
|
-
]), input.optimizationProfile ?? 'standard');
|
|
59
|
-
const selectedWorkflows = rankAssetSelectors(goal, matchedDomains, input.files ?? [], dedupeStrings([
|
|
60
|
-
...
|
|
60
|
+
]), input.optimizationProfile ?? 'standard'));
|
|
61
|
+
const selectedWorkflows = pinRequestedSelectors(requestedWorkflows, rankAssetSelectors(goal, matchedDomains, input.files ?? [], dedupeStrings([
|
|
62
|
+
...requestedWorkflows,
|
|
61
63
|
...selectedSpecialists.flatMap((entry) => getSpecialist(entry.specialistId)?.recommendedWorkflows ?? []),
|
|
62
|
-
]), input.optimizationProfile ?? 'standard');
|
|
64
|
+
]), input.optimizationProfile ?? 'standard'));
|
|
63
65
|
const toolPolicy = selectToolPolicy(selectedSpecialists);
|
|
64
66
|
const fallbackPlan = {
|
|
65
67
|
summary: 'Fallback to current runtime domain-pack execution if specialist confidence or crew coverage is weak.',
|
|
@@ -533,6 +535,12 @@ function rankAssetSelectors(goal, matchedDomains, files, values, optimizationPro
|
|
|
533
535
|
.slice(0, optimizationProfile === 'max' ? 6 : 4)
|
|
534
536
|
.map((entry) => entry.value);
|
|
535
537
|
}
|
|
538
|
+
function pinRequestedSelectors(requested, ranked) {
|
|
539
|
+
return dedupeStrings([
|
|
540
|
+
...requested,
|
|
541
|
+
...ranked,
|
|
542
|
+
]);
|
|
543
|
+
}
|
|
536
544
|
function dedupeStrings(values) {
|
|
537
545
|
return [...new Set(values.filter(Boolean))];
|
|
538
546
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const USER_WORKFLOW_TRACE_FILE = "user-workflow-trace.json";
|
|
2
|
+
export type UserWorkflowSpanKind = 'user_goal' | 'planning' | 'context' | 'agent_action' | 'verification' | 'decision' | 'outcome';
|
|
3
|
+
export type UserWorkflowStatus = 'ok' | 'warn' | 'failed' | 'skipped';
|
|
4
|
+
export interface UserWorkflowSpan {
|
|
5
|
+
id: string;
|
|
6
|
+
parentId?: string;
|
|
7
|
+
kind: UserWorkflowSpanKind;
|
|
8
|
+
label: string;
|
|
9
|
+
status: UserWorkflowStatus;
|
|
10
|
+
summary: string;
|
|
11
|
+
attributes?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface UserWorkflowSignal {
|
|
14
|
+
code: string;
|
|
15
|
+
severity: 'info' | 'warn' | 'high';
|
|
16
|
+
summary: string;
|
|
17
|
+
evidence?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface UserWorkflowTrace {
|
|
20
|
+
schemaVersion: 1;
|
|
21
|
+
scope: 'user-agent-workflow';
|
|
22
|
+
traceId: string;
|
|
23
|
+
runId: string;
|
|
24
|
+
generatedAt: number;
|
|
25
|
+
goal: string;
|
|
26
|
+
state: string;
|
|
27
|
+
result: string;
|
|
28
|
+
artifactsPath?: string;
|
|
29
|
+
userJourney: {
|
|
30
|
+
goal: string;
|
|
31
|
+
selectedFiles: string[];
|
|
32
|
+
selectedSkills: string[];
|
|
33
|
+
selectedWorkflows: string[];
|
|
34
|
+
selectedSpecialists: string[];
|
|
35
|
+
actions: string[];
|
|
36
|
+
verifyCommands: string[];
|
|
37
|
+
};
|
|
38
|
+
spans: UserWorkflowSpan[];
|
|
39
|
+
signals: UserWorkflowSignal[];
|
|
40
|
+
nextActions: string[];
|
|
41
|
+
}
|
|
42
|
+
type RunLike = Record<string, any>;
|
|
43
|
+
export declare function buildUserWorkflowTrace(run: RunLike): UserWorkflowTrace;
|
|
44
|
+
export declare function resolveUserWorkflowTracePath(artifactsPath: string): string;
|
|
45
|
+
export declare function readUserWorkflowTrace(artifactsPath: string): Promise<UserWorkflowTrace | undefined>;
|
|
46
|
+
export declare function summarizeUserWorkflowTrace(trace: UserWorkflowTrace): string;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export const USER_WORKFLOW_TRACE_FILE = 'user-workflow-trace.json';
|
|
4
|
+
function short(value, limit = 220) {
|
|
5
|
+
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
6
|
+
return text.length > limit ? `${text.slice(0, Math.max(0, limit - 1))}...` : text;
|
|
7
|
+
}
|
|
8
|
+
function uniqueStrings(values) {
|
|
9
|
+
return [...new Set(values.map((value) => String(value ?? '').trim()).filter(Boolean))];
|
|
10
|
+
}
|
|
11
|
+
function workerStatus(worker) {
|
|
12
|
+
if (worker.budgetExceeded)
|
|
13
|
+
return 'warn';
|
|
14
|
+
if (worker.verified)
|
|
15
|
+
return 'ok';
|
|
16
|
+
if (worker.outcome === 'failed')
|
|
17
|
+
return 'failed';
|
|
18
|
+
if (worker.outcome === 'partial')
|
|
19
|
+
return 'warn';
|
|
20
|
+
if (!String(worker.diff ?? '').trim() && !(worker.commandRecords ?? []).length)
|
|
21
|
+
return 'skipped';
|
|
22
|
+
return 'warn';
|
|
23
|
+
}
|
|
24
|
+
function verificationStatus(entry) {
|
|
25
|
+
if (entry.status === 'passed' || entry.passed === true)
|
|
26
|
+
return 'ok';
|
|
27
|
+
if (entry.status === 'skipped')
|
|
28
|
+
return 'skipped';
|
|
29
|
+
return 'failed';
|
|
30
|
+
}
|
|
31
|
+
function extractActions(run) {
|
|
32
|
+
const manifestActions = (run.workerManifests ?? [])
|
|
33
|
+
.flatMap((manifest) => manifest.actions ?? [])
|
|
34
|
+
.map((action) => action.command ?? action.type ?? action.name ?? action.tool ?? JSON.stringify(action));
|
|
35
|
+
const commandRecords = (run.workerResults ?? [])
|
|
36
|
+
.flatMap((worker) => worker.commandRecords ?? [])
|
|
37
|
+
.map((record) => record.command);
|
|
38
|
+
return uniqueStrings([...manifestActions, ...commandRecords]).slice(0, 30);
|
|
39
|
+
}
|
|
40
|
+
function extractVerifyCommands(run) {
|
|
41
|
+
const manifestCommands = (run.workerManifests ?? []).flatMap((manifest) => manifest.verifyCommands ?? []);
|
|
42
|
+
const verificationCommands = (run.verificationResults ?? [])
|
|
43
|
+
.flatMap((entry) => entry.commands ?? [])
|
|
44
|
+
.map((record) => record.command);
|
|
45
|
+
return uniqueStrings([...manifestCommands, ...verificationCommands]).slice(0, 30);
|
|
46
|
+
}
|
|
47
|
+
function buildSignals(run) {
|
|
48
|
+
const signals = [];
|
|
49
|
+
const workers = run.workerResults ?? [];
|
|
50
|
+
const verifications = run.verificationResults ?? [];
|
|
51
|
+
const modifiedFiles = workers.reduce((sum, worker) => sum + (worker.modifiedFiles?.length ?? 0), 0);
|
|
52
|
+
const hasDiff = workers.some((worker) => String(worker.diff ?? '').trim().length > 0);
|
|
53
|
+
const commandOnly = workers.some((worker) => (worker.commandRecords ?? []).length > 0);
|
|
54
|
+
const failedWorkers = workers.filter((worker) => workerStatus(worker) === 'failed');
|
|
55
|
+
const skippedVerifications = verifications.filter((entry) => entry.status === 'skipped');
|
|
56
|
+
const failedVerifications = verifications.filter((entry) => verificationStatus(entry) === 'failed');
|
|
57
|
+
if (String(run.state) === 'failed') {
|
|
58
|
+
signals.push({
|
|
59
|
+
code: 'workflow_failed',
|
|
60
|
+
severity: 'high',
|
|
61
|
+
summary: short(run.result || 'Agent workflow failed before a stable user outcome was produced.'),
|
|
62
|
+
evidence: { state: run.state },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (workers.length === 0) {
|
|
66
|
+
signals.push({
|
|
67
|
+
code: 'no_agent_workers',
|
|
68
|
+
severity: 'warn',
|
|
69
|
+
summary: 'No worker results were recorded for this user workflow.',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (failedWorkers.length > 0) {
|
|
73
|
+
signals.push({
|
|
74
|
+
code: 'agent_worker_failed',
|
|
75
|
+
severity: 'high',
|
|
76
|
+
summary: `${failedWorkers.length} worker(s) failed or returned unusable output.`,
|
|
77
|
+
evidence: { workers: failedWorkers.map((worker) => worker.workerId).slice(0, 10) },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (failedVerifications.length > 0) {
|
|
81
|
+
signals.push({
|
|
82
|
+
code: 'verification_failed',
|
|
83
|
+
severity: 'high',
|
|
84
|
+
summary: `${failedVerifications.length} verification step(s) failed.`,
|
|
85
|
+
evidence: { workers: failedVerifications.map((entry) => entry.workerId).slice(0, 10) },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!hasDiff && modifiedFiles === 0 && (skippedVerifications.length > 0 || commandOnly)) {
|
|
89
|
+
signals.push({
|
|
90
|
+
code: 'no_applicable_diff',
|
|
91
|
+
severity: String(run.state) === 'failed' ? 'warn' : 'info',
|
|
92
|
+
summary: 'Agents produced no repository patch; this is expected for read-only or command-only workflows.',
|
|
93
|
+
evidence: { skippedVerifications: skippedVerifications.length, commandOnly },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const tokenTelemetry = run.tokenTelemetry ?? {};
|
|
97
|
+
const grossTokens = Number(tokenTelemetry.grossInputTokens ?? 0);
|
|
98
|
+
const savedTokens = Number(tokenTelemetry.savedTokens ?? 0);
|
|
99
|
+
if (grossTokens > 80_000 && savedTokens < grossTokens * 0.2) {
|
|
100
|
+
signals.push({
|
|
101
|
+
code: 'token_pressure',
|
|
102
|
+
severity: 'warn',
|
|
103
|
+
summary: 'Workflow used a large context budget with low compression savings.',
|
|
104
|
+
evidence: { grossInputTokens: grossTokens, savedTokens },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return signals;
|
|
108
|
+
}
|
|
109
|
+
function buildNextActions(trace) {
|
|
110
|
+
if (trace.signals.length === 0) {
|
|
111
|
+
return ['Use this trace as the stable replay/eval baseline for similar user workflows.'];
|
|
112
|
+
}
|
|
113
|
+
const actions = new Set();
|
|
114
|
+
for (const signal of trace.signals) {
|
|
115
|
+
if (signal.code === 'workflow_failed' || signal.code === 'agent_worker_failed') {
|
|
116
|
+
actions.add('Inspect the failed worker span, then replay with narrower files, clearer success criteria, or a better specialist.');
|
|
117
|
+
}
|
|
118
|
+
if (signal.code === 'verification_failed') {
|
|
119
|
+
actions.add('Promote the failing verification commands into a regression eval before rerunning.');
|
|
120
|
+
}
|
|
121
|
+
if (signal.code === 'no_applicable_diff') {
|
|
122
|
+
actions.add('If a patch was expected, rerun with mutate intent and explicit target files; otherwise treat this as a read-only trace.');
|
|
123
|
+
}
|
|
124
|
+
if (signal.code === 'token_pressure') {
|
|
125
|
+
actions.add('Run token optimization first and replay with a smaller context packet.');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [...actions].slice(0, 5);
|
|
129
|
+
}
|
|
130
|
+
export function buildUserWorkflowTrace(run) {
|
|
131
|
+
const planner = run.plannerResult ?? {};
|
|
132
|
+
const selectedFiles = uniqueStrings(planner.selectedFiles ?? []);
|
|
133
|
+
const selectedSkills = uniqueStrings([
|
|
134
|
+
...(planner.selectedSkills ?? []),
|
|
135
|
+
...(run.activeSkills ?? []).map((skill) => skill.name),
|
|
136
|
+
]);
|
|
137
|
+
const selectedWorkflows = uniqueStrings([
|
|
138
|
+
...(planner.selectedWorkflows ?? []),
|
|
139
|
+
...(run.activeWorkflows ?? []).map((workflow) => workflow.name),
|
|
140
|
+
]);
|
|
141
|
+
const selectedSpecialists = uniqueStrings([
|
|
142
|
+
...(planner.selectedSpecialists ?? []).map((specialist) => specialist.name ?? specialist.specialistId),
|
|
143
|
+
...(run.workerManifests ?? []).map((manifest) => manifest.specialistName ?? manifest.specialistId),
|
|
144
|
+
]);
|
|
145
|
+
const actions = extractActions(run);
|
|
146
|
+
const verifyCommands = extractVerifyCommands(run);
|
|
147
|
+
const spans = [
|
|
148
|
+
{
|
|
149
|
+
id: 'user-goal',
|
|
150
|
+
kind: 'user_goal',
|
|
151
|
+
label: 'User goal',
|
|
152
|
+
status: 'ok',
|
|
153
|
+
summary: short(run.goal, 320),
|
|
154
|
+
attributes: {
|
|
155
|
+
parentRunId: run.parentRunId ?? null,
|
|
156
|
+
sourceAutomationId: run.sourceAutomationId ?? null,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'planning',
|
|
161
|
+
parentId: 'user-goal',
|
|
162
|
+
kind: 'planning',
|
|
163
|
+
label: 'Agent plan',
|
|
164
|
+
status: selectedSpecialists.length > 0 || selectedSkills.length > 0 ? 'ok' : 'warn',
|
|
165
|
+
summary: short(planner.summary ?? 'Planner selected agents, skills, workflows, and files for the user workflow.'),
|
|
166
|
+
attributes: { selectedSkills, selectedWorkflows, selectedSpecialists },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'context',
|
|
170
|
+
parentId: 'planning',
|
|
171
|
+
kind: 'context',
|
|
172
|
+
label: 'Context packet',
|
|
173
|
+
status: selectedFiles.length > 0 ? 'ok' : 'warn',
|
|
174
|
+
summary: `${selectedFiles.length} file(s), ${selectedSkills.length} skill(s), ${selectedWorkflows.length} workflow(s) routed into the agent workflow.`,
|
|
175
|
+
attributes: {
|
|
176
|
+
selectedFiles,
|
|
177
|
+
memoryChecks: run.memoryChecks?.length ?? 0,
|
|
178
|
+
tokenTelemetry: run.tokenTelemetry ?? null,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
...(run.workerResults ?? []).map((worker) => ({
|
|
182
|
+
id: `worker-${worker.workerId ?? 'unknown'}`,
|
|
183
|
+
parentId: 'planning',
|
|
184
|
+
kind: 'agent_action',
|
|
185
|
+
label: `${worker.role ?? 'worker'} ${worker.workerId ?? 'unknown'}`,
|
|
186
|
+
status: workerStatus(worker),
|
|
187
|
+
summary: short(worker.summary ?? worker.learned ?? worker.outcome ?? 'Agent worker completed.'),
|
|
188
|
+
attributes: {
|
|
189
|
+
workerId: worker.workerId,
|
|
190
|
+
role: worker.role,
|
|
191
|
+
outcome: worker.outcome,
|
|
192
|
+
verified: Boolean(worker.verified),
|
|
193
|
+
modifiedFiles: worker.modifiedFiles ?? [],
|
|
194
|
+
commandCount: worker.commandRecords?.length ?? 0,
|
|
195
|
+
tokensUsed: worker.tokensUsed ?? null,
|
|
196
|
+
},
|
|
197
|
+
})),
|
|
198
|
+
...(run.verificationResults ?? []).map((entry) => ({
|
|
199
|
+
id: `verification-${entry.verifierId ?? entry.workerId ?? 'unknown'}`,
|
|
200
|
+
parentId: `worker-${entry.workerId ?? 'unknown'}`,
|
|
201
|
+
kind: 'verification',
|
|
202
|
+
label: `Verification for ${entry.workerId ?? 'worker'}`,
|
|
203
|
+
status: verificationStatus(entry),
|
|
204
|
+
summary: short(entry.summary ?? 'Verification completed.'),
|
|
205
|
+
attributes: {
|
|
206
|
+
workerId: entry.workerId,
|
|
207
|
+
verifierId: entry.verifierId,
|
|
208
|
+
commandCount: entry.commands?.length ?? 0,
|
|
209
|
+
skippedReason: entry.skippedReason ?? null,
|
|
210
|
+
},
|
|
211
|
+
})),
|
|
212
|
+
{
|
|
213
|
+
id: 'decision',
|
|
214
|
+
parentId: 'planning',
|
|
215
|
+
kind: 'decision',
|
|
216
|
+
label: 'Merge decision',
|
|
217
|
+
status: run.finalDecision?.action === 'apply' ? 'ok' : run.finalDecision?.action ? 'warn' : 'skipped',
|
|
218
|
+
summary: short(run.finalDecision?.recommendedStrategy ?? run.finalDecision?.action ?? 'No merge decision recorded.'),
|
|
219
|
+
attributes: {
|
|
220
|
+
action: run.finalDecision?.action ?? null,
|
|
221
|
+
winner: run.finalDecision?.winner?.workerId ?? null,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'outcome',
|
|
226
|
+
parentId: 'decision',
|
|
227
|
+
kind: 'outcome',
|
|
228
|
+
label: 'User workflow outcome',
|
|
229
|
+
status: String(run.state) === 'failed' ? 'failed' : 'ok',
|
|
230
|
+
summary: short(run.result ?? ''),
|
|
231
|
+
attributes: {
|
|
232
|
+
state: run.state,
|
|
233
|
+
promotions: run.promotionDecisions?.length ?? 0,
|
|
234
|
+
continuationChildren: run.continuationChildren?.length ?? 0,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
const signals = buildSignals(run);
|
|
239
|
+
const trace = {
|
|
240
|
+
schemaVersion: 1,
|
|
241
|
+
scope: 'user-agent-workflow',
|
|
242
|
+
traceId: `uwt_${run.runId}`,
|
|
243
|
+
runId: String(run.runId ?? ''),
|
|
244
|
+
generatedAt: Date.now(),
|
|
245
|
+
goal: String(run.goal ?? ''),
|
|
246
|
+
state: String(run.state ?? ''),
|
|
247
|
+
result: String(run.result ?? ''),
|
|
248
|
+
artifactsPath: run.artifactsPath,
|
|
249
|
+
userJourney: {
|
|
250
|
+
goal: String(run.goal ?? ''),
|
|
251
|
+
selectedFiles,
|
|
252
|
+
selectedSkills,
|
|
253
|
+
selectedWorkflows,
|
|
254
|
+
selectedSpecialists,
|
|
255
|
+
actions,
|
|
256
|
+
verifyCommands,
|
|
257
|
+
},
|
|
258
|
+
spans,
|
|
259
|
+
signals,
|
|
260
|
+
nextActions: [],
|
|
261
|
+
};
|
|
262
|
+
trace.nextActions = buildNextActions(trace);
|
|
263
|
+
return trace;
|
|
264
|
+
}
|
|
265
|
+
export function resolveUserWorkflowTracePath(artifactsPath) {
|
|
266
|
+
return path.join(artifactsPath, USER_WORKFLOW_TRACE_FILE);
|
|
267
|
+
}
|
|
268
|
+
export async function readUserWorkflowTrace(artifactsPath) {
|
|
269
|
+
try {
|
|
270
|
+
const raw = await fs.readFile(resolveUserWorkflowTracePath(artifactsPath), 'utf8');
|
|
271
|
+
return JSON.parse(raw);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export function summarizeUserWorkflowTrace(trace) {
|
|
278
|
+
const high = trace.signals.filter((signal) => signal.severity === 'high').length;
|
|
279
|
+
const warns = trace.signals.filter((signal) => signal.severity === 'warn').length;
|
|
280
|
+
return `${trace.state.toUpperCase()} user workflow · ${trace.spans.length} span(s) · ${high} high signal(s), ${warns} warning(s)`;
|
|
281
|
+
}
|
|
@@ -8,6 +8,10 @@ import { type WorktreeHealthSnapshot } from '../../engines/worktree-health.js';
|
|
|
8
8
|
import type { MergeDecision } from '../index.js';
|
|
9
9
|
import type { RuntimeWorkerResult, WorkerRole } from '../runtime.js';
|
|
10
10
|
import { type CommandRecord } from './worker.js';
|
|
11
|
+
export interface BindingApplicationResult {
|
|
12
|
+
modifiedFiles: string[];
|
|
13
|
+
commandRecords: CommandRecord[];
|
|
14
|
+
}
|
|
11
15
|
/**
|
|
12
16
|
* Lightweight per-run artifact writer. Maintains a flat index of paths
|
|
13
17
|
* written so callers can recover the full run artifact layout.
|
|
@@ -36,11 +40,12 @@ export declare class WorktreeSession {
|
|
|
36
40
|
constructor(repoRoot: string, workerId: string, role: WorkerRole, recorder: ArtifactRecorder, reportHealth?: (snapshot: WorktreeHealthSnapshot) => void);
|
|
37
41
|
create(): Promise<void>;
|
|
38
42
|
run(command: string, allowFailure?: boolean, timeoutMs?: number): Promise<CommandRecord>;
|
|
39
|
-
applyBindings(bindings: SkillBinding[], timeoutMs?: number): Promise<
|
|
43
|
+
applyBindings(bindings: SkillBinding[], timeoutMs?: number): Promise<BindingApplicationResult>;
|
|
40
44
|
captureDiff(): Promise<string>;
|
|
41
45
|
applyPatchContent(diff: string): Promise<void>;
|
|
42
46
|
cleanup(): Promise<void>;
|
|
43
47
|
private resolve;
|
|
48
|
+
private linkWorkspaceDependencies;
|
|
44
49
|
}
|
|
45
50
|
/** Thin adapter that wraps a MemoryBackend for use inside MergeOracle. */
|
|
46
51
|
export declare class MergeMemoryAdapter {
|
|
@@ -75,6 +75,7 @@ export class WorktreeSession {
|
|
|
75
75
|
cwd: this.repoRoot,
|
|
76
76
|
maxBuffer: 1024 * 1024 * 20,
|
|
77
77
|
});
|
|
78
|
+
await this.linkWorkspaceDependencies();
|
|
78
79
|
}
|
|
79
80
|
catch (error) {
|
|
80
81
|
const retryHealth = await doctorGitWorktrees(this.repoRoot);
|
|
@@ -87,6 +88,7 @@ export class WorktreeSession {
|
|
|
87
88
|
}
|
|
88
89
|
async applyBindings(bindings, timeoutMs) {
|
|
89
90
|
const modified = new Set();
|
|
91
|
+
const commandRecords = [];
|
|
90
92
|
for (const binding of bindings) {
|
|
91
93
|
switch (binding.type) {
|
|
92
94
|
case 'write_file': {
|
|
@@ -114,12 +116,13 @@ export class WorktreeSession {
|
|
|
114
116
|
}
|
|
115
117
|
case 'run_command': {
|
|
116
118
|
const record = await this.run(binding.command || '', false, timeoutMs);
|
|
119
|
+
commandRecords.push(record);
|
|
117
120
|
this.recorder.writeJson(path.join('workers', this.workerId, `${sanitizeFileName(record.command)}.json`), record);
|
|
118
121
|
break;
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
|
-
return [...modified];
|
|
125
|
+
return { modifiedFiles: [...modified], commandRecords };
|
|
123
126
|
}
|
|
124
127
|
async captureDiff() {
|
|
125
128
|
try {
|
|
@@ -130,6 +133,12 @@ export class WorktreeSession {
|
|
|
130
133
|
catch {
|
|
131
134
|
// Runtime skill overlays stay outside repo patches.
|
|
132
135
|
}
|
|
136
|
+
try {
|
|
137
|
+
await exec('git reset HEAD -- node_modules', { cwd: this.worktreeDir, maxBuffer: 1024 * 1024 * 20 });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Shared dependency symlink stays outside repo patches.
|
|
141
|
+
}
|
|
133
142
|
const { stdout } = await exec('git diff --binary --cached HEAD', {
|
|
134
143
|
cwd: this.worktreeDir,
|
|
135
144
|
maxBuffer: 1024 * 1024 * 20,
|
|
@@ -167,6 +176,23 @@ export class WorktreeSession {
|
|
|
167
176
|
return target;
|
|
168
177
|
return path.join(this.worktreeDir, target);
|
|
169
178
|
}
|
|
179
|
+
async linkWorkspaceDependencies() {
|
|
180
|
+
const source = path.join(this.repoRoot, 'node_modules');
|
|
181
|
+
const target = path.join(this.worktreeDir, 'node_modules');
|
|
182
|
+
try {
|
|
183
|
+
const stat = await fs.promises.lstat(source);
|
|
184
|
+
if (!stat.isDirectory() && !stat.isSymbolicLink())
|
|
185
|
+
return;
|
|
186
|
+
await fs.promises.lstat(target).then(() => undefined).catch(async (error) => {
|
|
187
|
+
if (error?.code !== 'ENOENT')
|
|
188
|
+
throw error;
|
|
189
|
+
await fs.promises.symlink(source, target, 'dir');
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Worktrees remain valid for repos that do not use node_modules or have not installed deps.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
170
196
|
}
|
|
171
197
|
// ─── MergeMemoryAdapter ───────────────────────────────────────────────────────
|
|
172
198
|
/** Thin adapter that wraps a MemoryBackend for use inside MergeOracle. */
|
|
@@ -14,6 +14,7 @@ import { type ContinuationProposal, type FallbackPlan, type OptimizationProfile,
|
|
|
14
14
|
import { type TaskPlannerState } from '../engines/task-planner.js';
|
|
15
15
|
import { type RuntimeArtifactSelectionAudit, type RuntimeArtifactOutcomeSnapshot, type RuntimeCatalogHealthSnapshot, type RuntimeClientInstructionStatus, type RuntimeMemoryHealthSnapshot, type RuntimeMemoryReconciliationSummary, type RuntimeMemoryScopeUsageSnapshot, type RuntimeRagUsageSummary, type RuntimeRegistrySnapshot, type RuntimeOrchestrationSnapshot, type RuntimePrimaryClientSnapshot, type RuntimeSourceAwareTokenBudgetSnapshot, type RuntimeTaskGraphSnapshot, type RuntimeTokenRunSnapshot, type RuntimeTokenSummarySnapshot, type RuntimeWorkerPlanSnapshot } from '../engines/runtime-registry.js';
|
|
16
16
|
import { type ExecutionLedger, type InstructionPacket, type OrchestrationExecutionMode } from '../engines/instruction-gateway.js';
|
|
17
|
+
import { type UserWorkflowTrace } from '../engines/user-workflow-trace.js';
|
|
17
18
|
import type { KnowledgeFabricBundle, KnowledgeFabricSnapshot } from '../engines/knowledge-fabric.js';
|
|
18
19
|
import type { BootstrapManifestStatus } from '../engines/client-bootstrap.js';
|
|
19
20
|
import { type WorktreeHealthSnapshot } from '../engines/worktree-health.js';
|
|
@@ -199,6 +200,7 @@ export interface RuntimeWorkerResult extends WorkerResult {
|
|
|
199
200
|
verification?: WorkerVerification;
|
|
200
201
|
artifactsPath: string;
|
|
201
202
|
modifiedFiles: string[];
|
|
203
|
+
commandRecords?: CommandRecord[];
|
|
202
204
|
}
|
|
203
205
|
export interface PlannerResult {
|
|
204
206
|
summary: string;
|
|
@@ -281,6 +283,7 @@ export interface ExecutionRun {
|
|
|
281
283
|
memoryScopeUsage?: RuntimeMemoryScopeUsageSnapshot;
|
|
282
284
|
memoryReconciliationSummary?: RuntimeMemoryReconciliationSummary;
|
|
283
285
|
autoGhostPass?: GhostReport;
|
|
286
|
+
userWorkflowTrace?: UserWorkflowTrace;
|
|
284
287
|
}
|
|
285
288
|
export interface SubAgentRuntimeOptions {
|
|
286
289
|
repoRoot?: string;
|
|
@@ -327,6 +330,8 @@ export declare class SubAgentRuntime {
|
|
|
327
330
|
runNXL(goal: string, rawScript?: string, useCase?: string): Promise<ExecutionRun>;
|
|
328
331
|
listRuns(limit?: number): ExecutionRun[];
|
|
329
332
|
getRun(runId: string): Promise<ExecutionRun | undefined>;
|
|
333
|
+
getUserWorkflowTrace(runId: string): Promise<UserWorkflowTrace | undefined>;
|
|
334
|
+
private persistUserWorkflowTrace;
|
|
330
335
|
getRuntimeId(): string;
|
|
331
336
|
getUsageSnapshot(): RuntimeRegistrySnapshot;
|
|
332
337
|
getInstructionPacket(): InstructionPacket | undefined;
|
|
@@ -533,6 +538,7 @@ export declare class SubAgentRuntime {
|
|
|
533
538
|
private applyDecision;
|
|
534
539
|
private evaluatePromotions;
|
|
535
540
|
private resolveCandidateDiff;
|
|
541
|
+
private isCommandOnlyOperationalSuccess;
|
|
536
542
|
private resolveFileRefs;
|
|
537
543
|
private discoverTargetFiles;
|
|
538
544
|
private collectLibraryCounts;
|
package/dist/phantom/runtime.js
CHANGED
|
@@ -19,6 +19,7 @@ import { planTask } from '../engines/task-planner.js';
|
|
|
19
19
|
import { nexusEventBus } from '../engines/event-bus.js';
|
|
20
20
|
import { RuntimeRegistry, createEmptyUsageState, createEmptyTokenSummary, } from '../engines/runtime-registry.js';
|
|
21
21
|
import { InstructionGateway, createExecutionLedger, markExecutionLedgerStep, renderInstructionPacketMarkdown, } from '../engines/instruction-gateway.js';
|
|
22
|
+
import { buildUserWorkflowTrace, readUserWorkflowTrace, USER_WORKFLOW_TRACE_FILE, } from '../engines/user-workflow-trace.js';
|
|
22
23
|
import { resolveWorkspaceContext } from '../engines/workspace-resolver.js';
|
|
23
24
|
import { WorktreeDoctorError, } from '../engines/worktree-health.js';
|
|
24
25
|
// ─── Sub-module imports ────────────────────────────────────────────────────────
|
|
@@ -363,6 +364,7 @@ export class SubAgentRuntime {
|
|
|
363
364
|
});
|
|
364
365
|
run.executionLedger = task.executionLedger;
|
|
365
366
|
this.syncExecutionMetadata(run);
|
|
367
|
+
this.persistUserWorkflowTrace(run, recorder);
|
|
366
368
|
recorder.writeJson('run.json', run);
|
|
367
369
|
return run;
|
|
368
370
|
}
|
|
@@ -554,6 +556,7 @@ export class SubAgentRuntime {
|
|
|
554
556
|
run.state = 'failed';
|
|
555
557
|
run.result = 'Hooks blocked execution before mutation.';
|
|
556
558
|
this.attachLatestRun(runId, task.goal, run.state);
|
|
559
|
+
this.persistUserWorkflowTrace(run, recorder);
|
|
557
560
|
recorder.writeJson('run.json', run);
|
|
558
561
|
return run;
|
|
559
562
|
}
|
|
@@ -688,14 +691,15 @@ export class SubAgentRuntime {
|
|
|
688
691
|
: enforcedBlockingGate
|
|
689
692
|
? { applied: false, rolledBack: false, summary: `Review gate ${enforcedBlockingGate.gate} remains ${enforcedBlockingGate.status}.` }
|
|
690
693
|
: await this.applyDecision(recorder, { ...task, verifyCommands: beforeVerifyResolved.verifyCommands }, decision, consensusPolicy);
|
|
691
|
-
|
|
692
|
-
//
|
|
693
|
-
// a review gate enforced, or a
|
|
694
|
+
const commandOnlyOperationalSuccess = this.isCommandOnlyOperationalSuccess(task, decision);
|
|
695
|
+
// Read-only and command-only operational runs don't produce a diff to apply.
|
|
696
|
+
// Only mark 'failed' when a shield blocked, a review gate enforced, or a
|
|
697
|
+
// mutate run produced neither a patch nor successful command evidence.
|
|
694
698
|
const readOnlyIntent = task.intent === 'inspect' || task.intent === 'plan';
|
|
695
699
|
const failureExpected = preApplyShield.blocked || Boolean(enforcedBlockingGate);
|
|
696
700
|
run.state = applied.applied
|
|
697
701
|
? (applied.rolledBack ? 'rolled_back' : 'merged')
|
|
698
|
-
: (readOnlyIntent && !failureExpected ? 'inspected' : 'failed');
|
|
702
|
+
: ((readOnlyIntent || commandOnlyOperationalSuccess) && !failureExpected ? 'inspected' : 'failed');
|
|
699
703
|
// Surface completion on the SSE feed so the dashboard can move the
|
|
700
704
|
// run from "Running" to "Done" without a poll round-trip.
|
|
701
705
|
try {
|
|
@@ -814,6 +818,7 @@ export class SubAgentRuntime {
|
|
|
814
818
|
recorder.writeJson('planner-state.json', run.plannerState);
|
|
815
819
|
recorder.writeJson('planner-result.json', run.plannerResult);
|
|
816
820
|
recorder.writeJson('manifests.json', run.workerManifests);
|
|
821
|
+
this.persistUserWorkflowTrace(run, recorder);
|
|
817
822
|
recorder.writeJson('run.json', run);
|
|
818
823
|
// Persist run-level token telemetry to the global ledger so nexus_token_report
|
|
819
824
|
// and dashboard analytics can aggregate lifetime savings across sessions.
|
|
@@ -882,6 +887,25 @@ export class SubAgentRuntime {
|
|
|
882
887
|
return undefined;
|
|
883
888
|
}
|
|
884
889
|
}
|
|
890
|
+
async getUserWorkflowTrace(runId) {
|
|
891
|
+
const run = await this.getRun(runId);
|
|
892
|
+
if (!run)
|
|
893
|
+
return undefined;
|
|
894
|
+
return (await readUserWorkflowTrace(run.artifactsPath)) ?? buildUserWorkflowTrace(run);
|
|
895
|
+
}
|
|
896
|
+
persistUserWorkflowTrace(run, recorder) {
|
|
897
|
+
try {
|
|
898
|
+
const trace = buildUserWorkflowTrace(run);
|
|
899
|
+
run.userWorkflowTrace = trace;
|
|
900
|
+
if (recorder) {
|
|
901
|
+
recorder.writeJson(USER_WORKFLOW_TRACE_FILE, trace);
|
|
902
|
+
}
|
|
903
|
+
return trace;
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
885
909
|
getRuntimeId() {
|
|
886
910
|
return this.runtimeId;
|
|
887
911
|
}
|
|
@@ -2048,13 +2072,20 @@ export class SubAgentRuntime {
|
|
|
2048
2072
|
verified: false,
|
|
2049
2073
|
artifactsPath: workerDir,
|
|
2050
2074
|
modifiedFiles: [],
|
|
2075
|
+
commandRecords: [],
|
|
2051
2076
|
};
|
|
2052
2077
|
}
|
|
2053
|
-
const
|
|
2078
|
+
const bindingResult = await session.applyBindings(allowedActions, timeoutMs);
|
|
2079
|
+
const modifiedFiles = bindingResult.modifiedFiles;
|
|
2080
|
+
const commandRecords = bindingResult.commandRecords;
|
|
2054
2081
|
modifiedFiles.forEach(file => {
|
|
2055
2082
|
this.sessionDNA?.recordFileModified(file);
|
|
2056
2083
|
learnings.push(`Modified ${file}`);
|
|
2057
2084
|
});
|
|
2085
|
+
if (commandRecords.length > 0) {
|
|
2086
|
+
const passedCommands = commandRecords.filter((record) => record.exitCode === 0).length;
|
|
2087
|
+
learnings.push(`Ran ${commandRecords.length} command binding(s), ${passedCommands} passed.`);
|
|
2088
|
+
}
|
|
2058
2089
|
const diff = await session.captureDiff();
|
|
2059
2090
|
recorder.writeText(path.join('workers', manifest.workerId, 'diff.patch'), diff);
|
|
2060
2091
|
podNetwork.publish(manifest.workerId, `Produced ${modifiedFiles.length} modified files`, 0.8, ['#runtime-worker']);
|
|
@@ -2063,23 +2094,30 @@ export class SubAgentRuntime {
|
|
|
2063
2094
|
if (budgetExceeded) {
|
|
2064
2095
|
learnings.push(`Token budget exceeded: used ${estimatedTokens}, budget ${manifest.tokenBudget}`);
|
|
2065
2096
|
}
|
|
2066
|
-
|
|
2097
|
+
const commandOnlySuccess = !diff.trim()
|
|
2098
|
+
&& commandRecords.length > 0
|
|
2099
|
+
&& commandRecords.every((record) => record.exitCode === 0);
|
|
2100
|
+
nexusEventBus.emit('phantom.worker.complete', {
|
|
2101
|
+
workerId: manifest.workerId,
|
|
2102
|
+
confidence: budgetExceeded ? 0.1 : (diff.trim() ? 0.7 : (commandOnlySuccess ? 0.75 : 0.2)),
|
|
2103
|
+
});
|
|
2067
2104
|
return {
|
|
2068
2105
|
workerId: manifest.workerId,
|
|
2069
2106
|
role: manifest.role,
|
|
2070
2107
|
taskId: runId,
|
|
2071
2108
|
approach: manifest.strategy,
|
|
2072
2109
|
diff,
|
|
2073
|
-
outcome: budgetExceeded ? 'budgetExceeded' : (diff.trim() ? 'partial' : 'failed'),
|
|
2074
|
-
confidence: budgetExceeded ? 0.1 : (diff.trim() ? 0.68 : 0.18),
|
|
2110
|
+
outcome: budgetExceeded ? 'budgetExceeded' : (diff.trim() ? 'partial' : (commandOnlySuccess ? 'success' : 'failed')),
|
|
2111
|
+
confidence: budgetExceeded ? 0.1 : (diff.trim() ? 0.68 : (commandOnlySuccess ? 0.82 : 0.18)),
|
|
2075
2112
|
tokensUsed: estimatedTokens,
|
|
2076
2113
|
tokenEstimateSource: 'runtime-estimate',
|
|
2077
2114
|
budgetExceeded,
|
|
2078
2115
|
learnings,
|
|
2079
|
-
testsPassing: 0,
|
|
2116
|
+
testsPassing: commandRecords.filter((record) => record.exitCode === 0).length,
|
|
2080
2117
|
verified: false,
|
|
2081
2118
|
artifactsPath: workerDir,
|
|
2082
2119
|
modifiedFiles,
|
|
2120
|
+
commandRecords,
|
|
2083
2121
|
};
|
|
2084
2122
|
}
|
|
2085
2123
|
catch (error) {
|
|
@@ -2102,6 +2140,7 @@ export class SubAgentRuntime {
|
|
|
2102
2140
|
verified: false,
|
|
2103
2141
|
artifactsPath: workerDir,
|
|
2104
2142
|
modifiedFiles: [],
|
|
2143
|
+
commandRecords: [],
|
|
2105
2144
|
};
|
|
2106
2145
|
}
|
|
2107
2146
|
finally {
|
|
@@ -2133,11 +2172,25 @@ export class SubAgentRuntime {
|
|
|
2133
2172
|
const verifier = new WorktreeSession(this.repoRoot, `${runId}-${manifest.workerId}`, 'verifier', recorder, (snapshot) => this.recordWorktreeHealth(snapshot));
|
|
2134
2173
|
const records = [];
|
|
2135
2174
|
const artifactsPath = recorder.workerDir(manifest.workerId);
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2175
|
+
const commandRecords = target?.commandRecords ?? [];
|
|
2176
|
+
const commandOnlySuccess = !target?.diff?.trim()
|
|
2177
|
+
&& commandRecords.length > 0
|
|
2178
|
+
&& commandRecords.every((record) => record.exitCode === 0);
|
|
2179
|
+
// Skip worktree creation if there is no diff. Command-only operational
|
|
2180
|
+
// workers are verified from their command records; pure no-op workers
|
|
2181
|
+
// remain skipped rather than counted as implementation evidence.
|
|
2140
2182
|
if (!target?.diff?.trim()) {
|
|
2183
|
+
if (commandOnlySuccess) {
|
|
2184
|
+
return {
|
|
2185
|
+
workerId: manifest.targetWorkerId ?? 'unknown',
|
|
2186
|
+
verifierId: manifest.workerId,
|
|
2187
|
+
passed: true,
|
|
2188
|
+
status: 'passed',
|
|
2189
|
+
commands: commandRecords,
|
|
2190
|
+
summary: `Verifier accepted command-only evidence (${commandRecords.length} command binding(s) passed).`,
|
|
2191
|
+
artifactsPath,
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2141
2194
|
return {
|
|
2142
2195
|
workerId: manifest.targetWorkerId ?? 'unknown',
|
|
2143
2196
|
verifierId: manifest.workerId,
|
|
@@ -2209,10 +2262,25 @@ export class SubAgentRuntime {
|
|
|
2209
2262
|
async applyDecision(recorder, task, decision, consensusPolicy) {
|
|
2210
2263
|
const candidateDiff = this.resolveCandidateDiff(decision);
|
|
2211
2264
|
if (!candidateDiff.trim()) {
|
|
2265
|
+
if (this.isCommandOnlyOperationalSuccess(task, decision)) {
|
|
2266
|
+
const records = decision.winner?.commandRecords ?? [];
|
|
2267
|
+
return {
|
|
2268
|
+
applied: false,
|
|
2269
|
+
rolledBack: false,
|
|
2270
|
+
summary: `Operational workflow completed with command evidence only (${records.length} command binding(s) passed); no repository patch was required.`,
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
if (task.intent === 'inspect' || task.intent === 'plan') {
|
|
2274
|
+
return {
|
|
2275
|
+
applied: false,
|
|
2276
|
+
rolledBack: false,
|
|
2277
|
+
summary: 'Read-only worker swarm completed without a repository patch; no applicable diff was expected.',
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2212
2280
|
return {
|
|
2213
2281
|
applied: false,
|
|
2214
2282
|
rolledBack: false,
|
|
2215
|
-
summary:
|
|
2283
|
+
summary: 'Worker swarm completed without a repository patch; no applicable diff was produced.',
|
|
2216
2284
|
};
|
|
2217
2285
|
}
|
|
2218
2286
|
if (!consensusPolicy.approveRunLevelChange(decision.winner ? [decision.winner.workerId] : ['merge-oracle'])) {
|
|
@@ -2409,6 +2477,20 @@ export class SubAgentRuntime {
|
|
|
2409
2477
|
}
|
|
2410
2478
|
return decision.winner?.diff ?? '';
|
|
2411
2479
|
}
|
|
2480
|
+
isCommandOnlyOperationalSuccess(task, decision) {
|
|
2481
|
+
if (decision.action === 'reject')
|
|
2482
|
+
return false;
|
|
2483
|
+
if (this.resolveCandidateDiff(decision).trim())
|
|
2484
|
+
return false;
|
|
2485
|
+
const winner = decision.winner;
|
|
2486
|
+
const records = winner?.commandRecords ?? [];
|
|
2487
|
+
if (records.length === 0 || !records.every((record) => record.exitCode === 0)) {
|
|
2488
|
+
return false;
|
|
2489
|
+
}
|
|
2490
|
+
return task.releasePolicy.mode === 'ship-ready'
|
|
2491
|
+
|| task.workflowSelectors.some((selector) => /release|deploy|publish|ci|smoke|audit/i.test(selector))
|
|
2492
|
+
|| /release|publish|deploy|npm|github action|ci\/cd|smoke|audit/i.test(task.goal);
|
|
2493
|
+
}
|
|
2412
2494
|
async resolveFileRefs(files) {
|
|
2413
2495
|
return Promise.all(files.map(async (file) => {
|
|
2414
2496
|
const resolved = path.isAbsolute(file) ? file : path.join(this.repoRoot, file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-prime",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.9",
|
|
4
4
|
"description": "Local-first MCP control plane for coding agents with bootstrap-orchestrate execution, memory fabric, token budgeting, and worktree-backed swarms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|