nexus-prime 7.9.8 → 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.
@@ -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 skills = Array.isArray(request.params.arguments?.skills)
337
+ const explicitSkills = Array.isArray(request.params.arguments?.skills)
319
338
  ? request.params.arguments.skills.map(String)
320
- : undefined;
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: ${(execution.finalDecision?.action ?? 'none').padEnd(52, ' ')}`
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: ${execution.finalDecision?.action ?? 'none'}`,
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?.() ?? [],
@@ -235,6 +235,7 @@ export interface AssetsSurfaceDTO {
235
235
  usage: unknown;
236
236
  selectionAudit: unknown;
237
237
  featureRegistry: unknown;
238
+ patternPacks: unknown[];
238
239
  skills: unknown[];
239
240
  specialists: unknown[];
240
241
  crews: unknown[];
@@ -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,
@@ -2333,7 +2333,7 @@ export class OrchestratorEngine {
2333
2333
  authorityTier: this.getAuthorityTier('workflow'),
2334
2334
  })),
2335
2335
  ];
2336
- const hookSelection = this.resolveCatalogVotes('hook', task, intent, hookItems, {
2336
+ const hookSelection = this.resolveCatalogVotes('hook', task, intent, options.hookSelectors?.length ? allHookItems : hookItems, {
2337
2337
  explicit: options.hookSelectors,
2338
2338
  planner: [],
2339
2339
  knowledge: knowledgeFabric.recommendations.hooks,
@@ -2341,7 +2341,7 @@ export class OrchestratorEngine {
2341
2341
  limit: intent.riskClass === 'high' ? 3 : 2,
2342
2342
  selector: 'name',
2343
2343
  });
2344
- const automationSelection = this.resolveCatalogVotes('automation', task, intent, automationItems, {
2344
+ const automationSelection = this.resolveCatalogVotes('automation', task, intent, options.automationSelectors?.length ? allAutomationItems : automationItems, {
2345
2345
  explicit: options.automationSelectors,
2346
2346
  planner: [],
2347
2347
  knowledge: knowledgeFabric.recommendations.automations,
@@ -2349,7 +2349,7 @@ export class OrchestratorEngine {
2349
2349
  limit: intent.taskType === 'release' || this.sessionState.repeatedFailures > 0 ? 3 : 2,
2350
2350
  selector: 'name',
2351
2351
  });
2352
- const specialistSelection = this.resolveCatalogVotes('specialist', task, intent, specialistItems, {
2352
+ const specialistSelection = this.resolveCatalogVotes('specialist', task, intent, options.specialistSelectors?.length ? allSpecialistItems : specialistItems, {
2353
2353
  explicit: options.specialistSelectors,
2354
2354
  planner: planner.selectedSpecialists.map((specialist) => specialist.specialistId),
2355
2355
  knowledge: knowledgeFabric.recommendations.specialists,
@@ -2357,7 +2357,7 @@ export class OrchestratorEngine {
2357
2357
  limit: 4,
2358
2358
  selector: 'id',
2359
2359
  });
2360
- const crewSelection = this.resolveCatalogVotes('crew', task, intent, crewItems, {
2360
+ const crewSelection = this.resolveCatalogVotes('crew', task, intent, options.crewSelectors?.length ? allCrewItems : crewItems, {
2361
2361
  explicit: options.crewSelectors,
2362
2362
  planner: planner.selectedCrew ? [planner.selectedCrew.crewId] : [],
2363
2363
  knowledge: knowledgeFabric.recommendations.crews,
@@ -2509,7 +2509,19 @@ export class OrchestratorEngine {
2509
2509
  resolveCatalogVotes(kind, task, intent, items, input) {
2510
2510
  const explicit = dedupeStrings(input.explicit ?? []);
2511
2511
  if (explicit.length > 0) {
2512
- const selectedEntries = explicit.map((value) => this.toArtifactAuditEntry(kind, value, items, {
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, {
2513
2525
  score: 1,
2514
2526
  source: 'explicit',
2515
2527
  confidence: 'high',
@@ -2517,7 +2529,23 @@ export class OrchestratorEngine {
2517
2529
  selector: input.selector,
2518
2530
  selectionPolicy: 'explicit-only',
2519
2531
  }));
2520
- return { selectedValues: explicit, selectedEntries, rejectedEntries: [] };
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
+ };
2521
2549
  }
2522
2550
  const votes = new Map();
2523
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;
@@ -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
- ...(input.requestedSkills ?? []),
50
- ...(input.requestedWorkflows ?? []),
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
- ...(input.requestedSkills ?? []),
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
- ...(input.requestedWorkflows ?? []),
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
+ }
@@ -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';
@@ -282,6 +283,7 @@ export interface ExecutionRun {
282
283
  memoryScopeUsage?: RuntimeMemoryScopeUsageSnapshot;
283
284
  memoryReconciliationSummary?: RuntimeMemoryReconciliationSummary;
284
285
  autoGhostPass?: GhostReport;
286
+ userWorkflowTrace?: UserWorkflowTrace;
285
287
  }
286
288
  export interface SubAgentRuntimeOptions {
287
289
  repoRoot?: string;
@@ -328,6 +330,8 @@ export declare class SubAgentRuntime {
328
330
  runNXL(goal: string, rawScript?: string, useCase?: string): Promise<ExecutionRun>;
329
331
  listRuns(limit?: number): ExecutionRun[];
330
332
  getRun(runId: string): Promise<ExecutionRun | undefined>;
333
+ getUserWorkflowTrace(runId: string): Promise<UserWorkflowTrace | undefined>;
334
+ private persistUserWorkflowTrace;
331
335
  getRuntimeId(): string;
332
336
  getUsageSnapshot(): RuntimeRegistrySnapshot;
333
337
  getInstructionPacket(): InstructionPacket | undefined;
@@ -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
  }
@@ -815,6 +818,7 @@ export class SubAgentRuntime {
815
818
  recorder.writeJson('planner-state.json', run.plannerState);
816
819
  recorder.writeJson('planner-result.json', run.plannerResult);
817
820
  recorder.writeJson('manifests.json', run.workerManifests);
821
+ this.persistUserWorkflowTrace(run, recorder);
818
822
  recorder.writeJson('run.json', run);
819
823
  // Persist run-level token telemetry to the global ledger so nexus_token_report
820
824
  // and dashboard analytics can aggregate lifetime savings across sessions.
@@ -883,6 +887,25 @@ export class SubAgentRuntime {
883
887
  return undefined;
884
888
  }
885
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
+ }
886
909
  getRuntimeId() {
887
910
  return this.runtimeId;
888
911
  }
@@ -2247,10 +2270,17 @@ export class SubAgentRuntime {
2247
2270
  summary: `Operational workflow completed with command evidence only (${records.length} command binding(s) passed); no repository patch was required.`,
2248
2271
  };
2249
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
+ }
2250
2280
  return {
2251
2281
  applied: false,
2252
2282
  rolledBack: false,
2253
- summary: `Execution finished with ${decision.action} but no applicable diff was produced.`,
2283
+ summary: 'Worker swarm completed without a repository patch; no applicable diff was produced.',
2254
2284
  };
2255
2285
  }
2256
2286
  if (!consensusPolicy.approveRunLevelChange(decision.winner ? [decision.winner.workerId] : ['merge-oracle'])) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.9.8",
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",