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.
@@ -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[];
@@ -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 hookSelection = this.resolveCatalogVotes('hook', task, intent, hookItems, {
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: workflowSelection.selectedValues.length,
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
- ...workflowSelection.selectedEntries,
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: workflowSelection.selectedValues.length,
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: workflowSelection.selectedValues,
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 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, {
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
- 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
+ };
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
- ...(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
+ }
@@ -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<string[]>;
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;
@@ -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
- // Read-only intents (inspect/plan) don't produce a diff to apply, so applied=false
692
- // is the expected outcome not a failure. Only mark 'failed' when a shield blocked,
693
- // a review gate enforced, or a mutate run produced no applicable diff.
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 modifiedFiles = await session.applyBindings(allowedActions, timeoutMs);
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
- nexusEventBus.emit('phantom.worker.complete', { workerId: manifest.workerId, confidence: budgetExceeded ? 0.1 : (diff.trim() ? 0.7 : 0.2) });
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
- // Skip worktree creation if there is nothing to verify. An empty diff
2137
- // is not a verification failure — it means the worker produced no
2138
- // mutation (typical for inspect/plan intents). This is a skipped
2139
- // verification, not evidence that implementation passed.
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: `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.',
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.7",
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",