nexus-prime 7.9.17 → 7.9.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/adapters/mcp/definitions.js +3 -2
- package/dist/agents/adapters/mcp/handlers/orchestration.js +85 -13
- package/dist/agents/adapters/mcp/types.js +7 -0
- package/dist/agents/adapters/mcp.js +2 -0
- package/dist/cli/hook.js +3 -2
- package/dist/cli.js +2 -2
- package/dist/dashboard/app/styles/board.css +8 -0
- package/dist/dashboard/app/views/board.js +84 -18
- package/dist/dashboard/app/views/context-log.js +32 -3
- package/dist/dashboard/app/views/knowledge.js +104 -13
- package/dist/dashboard/app/views/workforce.js +56 -18
- package/dist/dashboard/routes/architects.js +9 -1
- package/dist/dashboard/routes/events.js +91 -1
- package/dist/dashboard/routes/governance.js +69 -19
- package/dist/dashboard/routes/memory.js +71 -16
- package/dist/dashboard/routes/runtime.js +158 -12
- package/dist/dashboard/routes/workforce.d.ts +34 -1
- package/dist/dashboard/routes/workforce.js +171 -0
- package/dist/dashboard/server.js +13 -4
- package/dist/dashboard/welcome.html +13 -13
- package/dist/engines/event-bus.d.ts +2 -0
- package/dist/engines/knowledge-fabric.d.ts +2 -8
- package/dist/engines/orchestrator.d.ts +2 -7
- package/dist/engines/orchestrator.js +11 -0
- package/dist/index.js +1 -0
- package/dist/utils/ascii-art.js +2 -0
- package/package.json +2 -2
|
@@ -292,8 +292,9 @@ export function buildMcpToolDefinitions() {
|
|
|
292
292
|
properties: {
|
|
293
293
|
goal: { type: 'string', description: 'What you are trying to accomplish' },
|
|
294
294
|
task: { type: 'string', description: 'Alias for goal (deprecated, use goal)' },
|
|
295
|
-
files: { type: 'array', items: { type: 'string' }, description: 'File paths to analyze. If omitted, auto-scans
|
|
296
|
-
budget: { type: 'number', description: 'Token budget override' }
|
|
295
|
+
files: { type: 'array', items: { type: 'string' }, description: 'File paths to analyze. If omitted, auto-scans a capped source shortlist instead of the whole repo.' },
|
|
296
|
+
budget: { type: 'number', description: 'Token budget override' },
|
|
297
|
+
maxFiles: { type: 'number', description: 'Maximum files to include when files are omitted (default 80, max 200)' }
|
|
297
298
|
},
|
|
298
299
|
required: [],
|
|
299
300
|
},
|
|
@@ -14,12 +14,28 @@ import { GhostPass, summarizeExecution } from '../../../../phantom/index.js';
|
|
|
14
14
|
import { applyExecutionPreset, resolveExecutionPreset } from '../../../../engines/execution-presets.js';
|
|
15
15
|
import { pLimit } from '../../../../engines/util/p-limit.js';
|
|
16
16
|
import { promises as fsPromises } from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
17
18
|
import { getSharedLicenseManager } from '../../../../licensing/index.js';
|
|
18
19
|
import { SessionDNAManager } from '../../../../engines/session-dna.js';
|
|
19
20
|
import { getSharedTelemetry } from '../../../../engines/telemetry-remote.js';
|
|
20
21
|
import { requireRuntime } from '../util/require-runtime.js';
|
|
21
22
|
import { ensureCrGraphBuilt } from '../../../../engines/code-review-graph-client.js';
|
|
22
23
|
import { recordFirstBootstrap } from '../../../../engines/telemetry.js';
|
|
24
|
+
function escapeMarkdownTableCell(value) {
|
|
25
|
+
return String(value ?? '')
|
|
26
|
+
.replace(/\r?\n/g, ' ')
|
|
27
|
+
.replace(/\|/g, '\\|')
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
function markdownTable(headers, rows) {
|
|
31
|
+
if (rows.length === 0)
|
|
32
|
+
return '';
|
|
33
|
+
return [
|
|
34
|
+
`| ${headers.map(escapeMarkdownTableCell).join(' | ')} |`,
|
|
35
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
36
|
+
...rows.map(row => `| ${row.map(escapeMarkdownTableCell).join(' | ')} |`),
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
23
39
|
export function extractSkillSelectorsFromPrompt(prompt) {
|
|
24
40
|
const selectors = new Set();
|
|
25
41
|
const add = (value) => {
|
|
@@ -282,6 +298,9 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
282
298
|
`Memory stats: prefrontal ${bootstrap.memoryStats?.prefrontal ?? 0} · hippocampus ${bootstrap.memoryStats?.hippocampus ?? 0} · cortex ${bootstrap.memoryStats?.cortex ?? 0}`,
|
|
283
299
|
`Recommended next step: ${bootstrap.recommendedNextStep || 'nexus_orchestrate'}`,
|
|
284
300
|
`Token optimization: ${bootstrap.tokenOptimization?.autoApplied ? `auto-applied — saved ${Number(bootstrap.tokenOptimization?.planMetrics?.savings ?? 0).toLocaleString()} tokens (${bootstrap.tokenOptimization?.planMetrics?.pct ?? 0}% reduction)` : (bootstrap.tokenOptimization?.required ? 'required before broad reading' : 'not required yet')}`,
|
|
301
|
+
bootstrap.tokenOptimization?.planMetrics
|
|
302
|
+
? `Token budget: original ${Number(bootstrap.tokenOptimization.planMetrics.originalTokens ?? 0).toLocaleString()} → planned ${Number(bootstrap.tokenOptimization.planMetrics.compressedTokens ?? 0).toLocaleString()} across ${Number(bootstrap.tokenOptimization.planMetrics.files ?? 0).toLocaleString()} file action(s)`
|
|
303
|
+
: '',
|
|
285
304
|
`Catalog health: ${bootstrap.catalogHealth?.overall || 'unknown'} · selected ${bootstrap.artifactSelectionAudit?.selected?.length || 0}`,
|
|
286
305
|
(() => {
|
|
287
306
|
try {
|
|
@@ -585,8 +604,13 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
585
604
|
const rawFiles = Array.isArray(request.params.arguments?.files)
|
|
586
605
|
? request.params.arguments.files.map(String)
|
|
587
606
|
: null;
|
|
588
|
-
const
|
|
589
|
-
|
|
607
|
+
const workspaceRoot = hctx.getWorkspace(request.params.arguments ?? {}).repoRoot;
|
|
608
|
+
const scannedFilePaths = rawFiles
|
|
609
|
+
?? await hctx.scanSourceFiles(workspaceRoot);
|
|
610
|
+
const requestedMaxFiles = Number(request.params.arguments?.maxFiles ?? request.params.arguments?.limit ?? 80);
|
|
611
|
+
const maxFiles = Math.max(1, Math.min(Number.isFinite(requestedMaxFiles) ? Math.floor(requestedMaxFiles) : 80, 200));
|
|
612
|
+
const filePaths = rawFiles ? scannedFilePaths : scannedFilePaths.slice(0, maxFiles);
|
|
613
|
+
const omittedFiles = rawFiles ? 0 : Math.max(0, scannedFilePaths.length - filePaths.length);
|
|
590
614
|
const statLimit = pLimit(8);
|
|
591
615
|
const files = await Promise.all(filePaths.map((p) => statLimit(async () => {
|
|
592
616
|
const resolved = hctx.resolveToolPath(p, request.params.arguments ?? {});
|
|
@@ -660,10 +684,14 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
660
684
|
const dedupLine = dedupTokens > 0
|
|
661
685
|
? `\n ${dedupTokens.toLocaleString()} tok deduped (${delta.unchanged.length} unchanged files)`
|
|
662
686
|
: '';
|
|
687
|
+
const scanLine = rawFiles
|
|
688
|
+
? `\n explicit file set: ${filePaths.length.toLocaleString()} file(s)`
|
|
689
|
+
: `\n auto-scan capped: ${filePaths.length.toLocaleString()}/${scannedFilePaths.length.toLocaleString()} file(s)${omittedFiles ? ` (${omittedFiles.toLocaleString()} omitted; pass files or maxFiles to override)` : ''}`;
|
|
663
690
|
const receipt = [
|
|
664
691
|
'',
|
|
665
692
|
`▸ Receipt ${plan.totalEstimatedTokens.toLocaleString()} tok in`,
|
|
666
693
|
` ${plan.savings.toLocaleString()} tok saved (${pct}% vs full read)${dedupLine}`,
|
|
694
|
+
scanLine.trimStart(),
|
|
667
695
|
` ~$${estimatedCostUsd.toFixed(4)} estimated · ~$${savedCostUsd.toFixed(4)} saved`,
|
|
668
696
|
].join('\n');
|
|
669
697
|
return { content: [{ type: 'text', text: formatted + receipt + notification + nudge }] };
|
|
@@ -673,7 +701,9 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
673
701
|
const ctx = {
|
|
674
702
|
action: String(args?.action ?? ''),
|
|
675
703
|
tokenCount: args?.tokenCount,
|
|
676
|
-
filesToModify: args?.filesToModify
|
|
704
|
+
filesToModify: Array.isArray(args?.filesToModify)
|
|
705
|
+
? args.filesToModify.map(String)
|
|
706
|
+
: (Array.isArray(args?.files) ? args.files.map(String) : undefined),
|
|
677
707
|
isDestructive: args?.isDestructive,
|
|
678
708
|
};
|
|
679
709
|
const result = getGuardrailEngine().check(ctx);
|
|
@@ -685,15 +715,47 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
685
715
|
const nudge = result.passed
|
|
686
716
|
? hctx.telemetry.planningNudge('high_call_count', {})
|
|
687
717
|
: hctx.telemetry.planningNudge('mindkit_fail', {});
|
|
718
|
+
const verdict = result.passed ? 'PASS' : 'FAIL';
|
|
719
|
+
const violationRows = result.violations.map((v) => {
|
|
720
|
+
const item = v;
|
|
721
|
+
return [
|
|
722
|
+
item.id ?? 'violation',
|
|
723
|
+
item.severity ?? 'high',
|
|
724
|
+
item.message ?? item.description ?? 'blocked by guardrail',
|
|
725
|
+
];
|
|
726
|
+
});
|
|
727
|
+
const warningRows = result.warnings.map((w) => {
|
|
728
|
+
const item = w;
|
|
729
|
+
return [
|
|
730
|
+
item.id ?? 'warning',
|
|
731
|
+
item.severity ?? 'warn',
|
|
732
|
+
item.message ?? item.description ?? 'review recommended',
|
|
733
|
+
];
|
|
734
|
+
});
|
|
735
|
+
const detailJson = JSON.stringify({
|
|
736
|
+
passed: result.passed,
|
|
737
|
+
score: Math.round(result.score * 100),
|
|
738
|
+
filesToModify: ctx.filesToModify ?? [],
|
|
739
|
+
violations: result.violations,
|
|
740
|
+
warnings: result.warnings,
|
|
741
|
+
summary: getGuardrailEngine().format(result)
|
|
742
|
+
}, null, 2);
|
|
688
743
|
return {
|
|
689
744
|
content: [{
|
|
690
|
-
type: 'text',
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
745
|
+
type: 'text',
|
|
746
|
+
text: [
|
|
747
|
+
`Mindkit check: ${verdict}`,
|
|
748
|
+
formatBullets([
|
|
749
|
+
`Score: ${Math.round(result.score * 100)}/100`,
|
|
750
|
+
`Files: ${ctx.filesToModify?.length ? ctx.filesToModify.join(', ') : 'none declared'}`,
|
|
751
|
+
`Decision: ${result.passed ? 'proceed' : 'stop and resolve guardrail violations'}`,
|
|
752
|
+
`Summary: ${getGuardrailEngine().format(result)}`,
|
|
753
|
+
]),
|
|
754
|
+
violationRows.length ? `Violations\n${markdownTable(['Rule', 'Severity', 'Message'], violationRows)}` : '',
|
|
755
|
+
warningRows.length ? `Warnings\n${markdownTable(['Rule', 'Severity', 'Message'], warningRows)}` : '',
|
|
756
|
+
`Details\n\`\`\`json\n${detailJson}\n\`\`\``,
|
|
757
|
+
nudge,
|
|
758
|
+
].filter(Boolean).join('\n\n')
|
|
697
759
|
}]
|
|
698
760
|
};
|
|
699
761
|
}
|
|
@@ -717,6 +779,17 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
717
779
|
hctx.nexusRef.storeMemory(`Ghost pass for "${goal.slice(0, 80)}": ${report.riskAreas.length} risks identified.`, 0.6, ['#ghost-pass']);
|
|
718
780
|
nexusEventBus.emit('ghost.pass', { task: goal, risks: report.riskAreas.length, workers: report.workerAssignments.length });
|
|
719
781
|
const ghostNudge = hctx.telemetry.planningNudge('ghost_pass', { risks: report.riskAreas.length });
|
|
782
|
+
const workerTable = markdownTable(['Worker', 'Approach', 'Token Budget', 'Files'], report.workerAssignments.map((worker, index) => [
|
|
783
|
+
`Worker ${index + 1}`,
|
|
784
|
+
worker.approach,
|
|
785
|
+
worker.tokenBudget.toLocaleString(),
|
|
786
|
+
worker.files.map(file => path.basename(file.path)).slice(0, 4).join(', ') || 'n/a',
|
|
787
|
+
]));
|
|
788
|
+
const riskTable = markdownTable(['Risk', 'Severity', 'Mitigation'], report.riskAreas.map((risk) => [
|
|
789
|
+
risk,
|
|
790
|
+
'review',
|
|
791
|
+
'Use the reading plan and isolate edits before mutation',
|
|
792
|
+
]));
|
|
720
793
|
// Console ASCII UI
|
|
721
794
|
const rCount = report.riskAreas.length;
|
|
722
795
|
hctx.box('👻 GHOST PASS PRE-FLIGHT', [
|
|
@@ -735,10 +808,9 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
735
808
|
`Risks: ${report.riskAreas.length ? report.riskAreas.join(' | ') : 'none detected'}`,
|
|
736
809
|
`Worker approaches: ${report.workerAssignments.length}`,
|
|
737
810
|
]),
|
|
811
|
+
riskTable ? `Risk table\n${riskTable}` : 'Risk table\nNo risks detected.',
|
|
812
|
+
workerTable ? `Worker table\n${workerTable}` : '',
|
|
738
813
|
'Reading plan\n```txt\n' + formatReadingPlan(report.readingPlan) + '\n```',
|
|
739
|
-
report.workerAssignments.length
|
|
740
|
-
? formatBullets(report.workerAssignments.map((worker, index) => `Worker ${index + 1}: ${worker.approach} · budget ${worker.tokenBudget.toLocaleString()} tokens`))
|
|
741
|
-
: '',
|
|
742
814
|
ghostNudge,
|
|
743
815
|
].filter(Boolean).join('\n\n'),
|
|
744
816
|
}],
|
|
@@ -120,6 +120,13 @@ export class SessionTelemetry {
|
|
|
120
120
|
this.noteFileIntent(args.files);
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
|
+
if (toolName === 'nexus_spawn_workers') {
|
|
124
|
+
if (this.lifecyclePhase === 'bootstrapped' || this.lifecyclePhase === 'orchestrated') {
|
|
125
|
+
this.advancePhase('working');
|
|
126
|
+
}
|
|
127
|
+
this.noteFileIntent(args.files);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
123
130
|
if (toolName === 'nexus_store_memory') {
|
|
124
131
|
if (this.isPostOrchestratePhase()) {
|
|
125
132
|
this.storeMemoryCalledPostOrchestrate = true;
|
|
@@ -136,6 +136,7 @@ const PRE_ORCHESTRATE_ALLOWED_TOOLS = new Set([
|
|
|
136
136
|
'nexus_optimize_tokens',
|
|
137
137
|
'nexus_mindkit_check',
|
|
138
138
|
'nexus_ghost_pass',
|
|
139
|
+
'nexus_spawn_workers',
|
|
139
140
|
'nexus_token_report',
|
|
140
141
|
'nexus_session_dna',
|
|
141
142
|
'nexus_run_status',
|
|
@@ -545,6 +546,7 @@ export class MCPAdapter {
|
|
|
545
546
|
code: 'orchestration-required',
|
|
546
547
|
reason: 'nexus_orchestrate has not been called for this session',
|
|
547
548
|
action: 'Call nexus_orchestrate(prompt="<your task>") before invoking mutation, worker, kernel, hook, automation, or low-level execution tools.',
|
|
549
|
+
nextTool: 'nexus_orchestrate',
|
|
548
550
|
hint: 'Nexus Prime must own planning, token budgeting, memory recall, hooks, and review gates before work starts.',
|
|
549
551
|
}, null, 2),
|
|
550
552
|
}],
|
package/dist/cli/hook.js
CHANGED
|
@@ -209,8 +209,9 @@ export async function runHookMindkit() {
|
|
|
209
209
|
if (!lock)
|
|
210
210
|
return; // no daemon — skip rather than block
|
|
211
211
|
const resp = await callDaemonTool(lock, 'nexus_mindkit_check', {
|
|
212
|
-
action: toolName,
|
|
213
|
-
files,
|
|
212
|
+
action: `${toolName}: ${files.join(', ')}`,
|
|
213
|
+
filesToModify: files,
|
|
214
|
+
isDestructive: ['Write', 'Edit', 'MultiEdit'].includes(toolName),
|
|
214
215
|
}, 6_000);
|
|
215
216
|
// Parse the tool result text to check for a block verdict
|
|
216
217
|
const text = extractResultText(resp);
|
package/dist/cli.js
CHANGED
|
@@ -717,11 +717,11 @@ program
|
|
|
717
717
|
.description('Start Nexus Prime daemon')
|
|
718
718
|
.option('--force', 'Force restart if already running')
|
|
719
719
|
.action(async (options) => {
|
|
720
|
-
const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
|
|
721
720
|
try {
|
|
722
721
|
const record = await ensureDaemonManaged({ force: options.force });
|
|
722
|
+
const dashboardPort = process.env.NEXUS_DASHBOARD_PORT ?? '3377';
|
|
723
723
|
console.log(`Nexus Prime daemon running (pid ${record.pid}, ${formatDaemonAddress(record)})`);
|
|
724
|
-
console.log(`Dashboard: http://localhost
|
|
724
|
+
console.log(`Dashboard: http://localhost:${dashboardPort}`);
|
|
725
725
|
}
|
|
726
726
|
catch (err) {
|
|
727
727
|
console.error(`Failed to start daemon: ${err.message}`);
|
|
@@ -395,6 +395,12 @@
|
|
|
395
395
|
color: var(--text-main); margin-bottom: 4px;
|
|
396
396
|
}
|
|
397
397
|
.frh-sub { font-size: var(--text-sm); color: var(--text-dim); }
|
|
398
|
+
.frh-command {
|
|
399
|
+
display: grid;
|
|
400
|
+
grid-template-columns: minmax(220px, 1fr) auto auto;
|
|
401
|
+
gap: 8px;
|
|
402
|
+
margin-bottom: 14px;
|
|
403
|
+
}
|
|
398
404
|
.frh-picks { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
|
399
405
|
.frh-pick {
|
|
400
406
|
background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius);
|
|
@@ -417,6 +423,8 @@
|
|
|
417
423
|
}
|
|
418
424
|
@media (max-width: 768px) {
|
|
419
425
|
#kanban-board { grid-template-columns: repeat(2, 1fr); }
|
|
426
|
+
.frh-command,
|
|
427
|
+
.frh-picks { grid-template-columns: 1fr; }
|
|
420
428
|
}
|
|
421
429
|
@media (max-width: 480px) {
|
|
422
430
|
#kanban-board { grid-template-columns: 1fr; }
|
|
@@ -76,12 +76,14 @@ function normalizeOperative(op) {
|
|
|
76
76
|
|
|
77
77
|
/* ── Data loader ── */
|
|
78
78
|
export async function load() {
|
|
79
|
-
const [tok, life, op, sh, health] = await Promise.all([
|
|
79
|
+
const [tok, life, op, sh, health, memHealth, runs] = await Promise.all([
|
|
80
80
|
api('/api/tokens/summary'),
|
|
81
81
|
api('/api/tokens/lifetime', 15000),
|
|
82
82
|
api('/api/dashboard/surface/operate', 5000),
|
|
83
83
|
api('/api/synapse/health', 5000),
|
|
84
84
|
api('/api/health', 15000),
|
|
85
|
+
api('/api/memory/health', 15000),
|
|
86
|
+
api('/api/runs?limit=12', 3000),
|
|
85
87
|
]);
|
|
86
88
|
// Non-blocking: tool health data from ring buffer
|
|
87
89
|
loadToolHealth();
|
|
@@ -91,6 +93,8 @@ export async function load() {
|
|
|
91
93
|
S.synapseHealthRaw = sh;
|
|
92
94
|
S.synapseHealth = (Array.isArray(sh) ? sh : (sh?.operatives||[])).map(normalizeOperative);
|
|
93
95
|
S.healthData = health;
|
|
96
|
+
S.memHealth = memHealth;
|
|
97
|
+
S.runs = Array.isArray(runs) ? runs : [];
|
|
94
98
|
notifyNotReady([sh]);
|
|
95
99
|
// Prefetch curated specialists for first-run hero (non-blocking)
|
|
96
100
|
if (!S.synapseHealth.length && !S.curatedSpecialists) {
|
|
@@ -274,6 +278,7 @@ function renderHero() {
|
|
|
274
278
|
?? lt?.totalSaved
|
|
275
279
|
?? lt?.lifetime?.saved
|
|
276
280
|
?? op?.tokenOptimization?.savedTokens
|
|
281
|
+
?? op?.tokenOptimization?.estimatedSavings
|
|
277
282
|
?? t?.savedTokens
|
|
278
283
|
?? t?.saved
|
|
279
284
|
?? 0,
|
|
@@ -317,14 +322,18 @@ function getHireReadiness() {
|
|
|
317
322
|
const synapseState = S.healthData?.runtimeEnvelope?.engines?.synapse ?? {};
|
|
318
323
|
const memoryStorage = S.healthData?.memory?.storage ?? {};
|
|
319
324
|
const rawReason = S.synapseHealthRaw?.reason;
|
|
325
|
+
const deferred = synapseState.deferred === true || /deferred\s*\(lazy mode\)/i.test(`${rawReason || ''} ${synapseState.reason || ''}`);
|
|
320
326
|
const unavailable = Boolean(
|
|
321
|
-
S.synapseHealthRaw?.notReady
|
|
322
|
-
|| synapseState.ready === false
|
|
323
|
-
|| synapseState.available === false,
|
|
327
|
+
(S.synapseHealthRaw?.notReady && !deferred)
|
|
328
|
+
|| (synapseState.ready === false && !deferred)
|
|
329
|
+
|| (synapseState.available === false && !deferred),
|
|
324
330
|
);
|
|
325
331
|
if (unavailable) {
|
|
326
332
|
notes.push({ tone: 'bad', text: rawReason || synapseState.reason || 'Synapse is not ready for hires in this dashboard session.' });
|
|
327
333
|
}
|
|
334
|
+
if (deferred && !unavailable) {
|
|
335
|
+
notes.push({ tone: 'warn', text: 'Synapse is in lazy mode and will warm up on the first hire.' });
|
|
336
|
+
}
|
|
328
337
|
if (synapseState.fallbackApplied) {
|
|
329
338
|
notes.push({ tone: 'warn', text: synapseState.reason || 'Synapse is running in fallback storage mode.' });
|
|
330
339
|
}
|
|
@@ -349,13 +358,14 @@ function renderFirstRunHero() {
|
|
|
349
358
|
if (!parent) return;
|
|
350
359
|
|
|
351
360
|
const hasOps = S.synapseHealth.length > 0;
|
|
361
|
+
const hasRuns = (S.runs || []).length > 0;
|
|
352
362
|
const alreadySeen = (() => { try { return !!localStorage.getItem(FIRST_RUN_KEY); } catch { return false; } })();
|
|
353
363
|
|
|
354
364
|
// Remove any existing hero card
|
|
355
365
|
const existing = $('first-run-hero');
|
|
356
366
|
if (existing) existing.remove();
|
|
357
367
|
|
|
358
|
-
if (hasOps || alreadySeen) return;
|
|
368
|
+
if (hasOps || (alreadySeen && hasRuns)) return;
|
|
359
369
|
|
|
360
370
|
const specs = (S.curatedSpecialists || []).slice(0, 3);
|
|
361
371
|
if (!specs.length) return; // Still loading — will re-render when prefetch resolves
|
|
@@ -371,10 +381,15 @@ function renderFirstRunHero() {
|
|
|
371
381
|
card.className = 'first-run-hero card';
|
|
372
382
|
card.innerHTML = `
|
|
373
383
|
<div class="frh-header">
|
|
374
|
-
<div class="frh-title"
|
|
375
|
-
<div class="frh-sub"
|
|
384
|
+
<div class="frh-title">${hasRuns ? 'Your next hire' : 'Start the first real run'}</div>
|
|
385
|
+
<div class="frh-sub">${hasRuns ? 'Hire a specialist to start running tasks autonomously.' : 'Run a goal from the dashboard or hire a specialist. The run will appear in Board and Context Log.'}</div>
|
|
376
386
|
</div>
|
|
377
387
|
${noticesHtml}
|
|
388
|
+
<div class="frh-command">
|
|
389
|
+
<input id="frh-goal-input" class="form-input" type="text" placeholder="Inspect this repo and suggest the next fix" autocomplete="off">
|
|
390
|
+
<button class="btn btn-primary btn-sm" id="frh-run-btn">Run goal</button>
|
|
391
|
+
<button class="btn btn-sm" id="frh-context-btn">Open context</button>
|
|
392
|
+
</div>
|
|
378
393
|
<div class="frh-picks">
|
|
379
394
|
${specs.map(s => `
|
|
380
395
|
<div class="frh-pick" data-specid="${esc(s.specialistId)}" data-specname="${esc(s.name)}">
|
|
@@ -388,6 +403,33 @@ function renderFirstRunHero() {
|
|
|
388
403
|
<button class="btn btn-ghost btn-sm frh-dismiss" style="margin-top:var(--space-3)">Dismiss</button>`;
|
|
389
404
|
|
|
390
405
|
// Wire buttons before inserting
|
|
406
|
+
card.querySelector('#frh-run-btn')?.addEventListener('click', async () => {
|
|
407
|
+
const input = card.querySelector('#frh-goal-input');
|
|
408
|
+
const button = card.querySelector('#frh-run-btn');
|
|
409
|
+
const goal = (input?.value || `Inspect ${S.workspace?.repoName || 'this repo'} and report the next best action`).trim();
|
|
410
|
+
if (!goal) return;
|
|
411
|
+
if (button) {
|
|
412
|
+
button.disabled = true;
|
|
413
|
+
button.textContent = 'Queueing…';
|
|
414
|
+
}
|
|
415
|
+
setFirstRunStatus('Queueing dashboard run…');
|
|
416
|
+
const result = await post('/api/orchestrate', { goal, source: 'dashboard-onboarding' });
|
|
417
|
+
if (result.ok) {
|
|
418
|
+
setFirstRunStatus('Run queued. Board and Context Log will update as Nexus writes artifacts.');
|
|
419
|
+
bustCache('/api/runs?limit=12');
|
|
420
|
+
bustCache('/api/events');
|
|
421
|
+
setTimeout(load, 900);
|
|
422
|
+
} else {
|
|
423
|
+
setFirstRunStatus(result.error || 'Run failed to queue.', 'bad');
|
|
424
|
+
if (button) {
|
|
425
|
+
button.disabled = false;
|
|
426
|
+
button.textContent = 'Run goal';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
card.querySelector('#frh-context-btn')?.addEventListener('click', () => {
|
|
431
|
+
window.location.hash = '#context-log';
|
|
432
|
+
});
|
|
391
433
|
card.querySelectorAll('.frh-hire-btn').forEach(btn => {
|
|
392
434
|
btn.addEventListener('click', async e => {
|
|
393
435
|
e.stopPropagation();
|
|
@@ -401,6 +443,7 @@ function renderFirstRunHero() {
|
|
|
401
443
|
specialistId: btn.dataset.specid,
|
|
402
444
|
name: btn.dataset.specname,
|
|
403
445
|
budgetCapUsd: 2,
|
|
446
|
+
fireFirstSortie: true,
|
|
404
447
|
});
|
|
405
448
|
if (result.ok) {
|
|
406
449
|
setFirstRunStatus(readiness.notes.some(note => note.tone === 'warn')
|
|
@@ -446,18 +489,41 @@ function renderAgentsLiveStrip() {
|
|
|
446
489
|
/* ── Kanban ── */
|
|
447
490
|
function buildKanbanCols() {
|
|
448
491
|
const cols={planning:[],hiring:[],running:[],ghostpass:[],done:[]};
|
|
449
|
-
const op=S.operateSurface;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
492
|
+
const op=S.operateSurface;
|
|
493
|
+
if (op) {
|
|
494
|
+
const pc=op.orchestration?.planningContext||op.planningContext;
|
|
495
|
+
if (pc?.goal) cols.planning.push({id:'ctx',goal:pc.goal,status:'planning',tokens:null,time:pc.startedAt,role:null});
|
|
496
|
+
const ws=op.orchestration?.workerPlan?.workers||op.workerPlan?.workers||[];
|
|
497
|
+
for (const w of ws) {
|
|
498
|
+
const st=String(w.status||'').toLowerCase();
|
|
499
|
+
const sg=st==='active'||st==='running'?'running':st==='hiring'?'hiring':st==='complete'||st==='done'?'done':st==='reviewing'||st.includes('ghost')?'ghostpass':null;
|
|
500
|
+
if (sg) cols[sg].push({id:w.id||w.workerId||w.goal,goal:w.goal||w.task||w.approach||'(worker)',status:st,tokens:w.tokensUsed||w.budget,time:w.startedAt||w.createdAt,role:w.role});
|
|
501
|
+
}
|
|
457
502
|
}
|
|
458
|
-
for (const r of (S.runs||[]).slice(0,
|
|
459
|
-
|
|
460
|
-
|
|
503
|
+
for (const r of (S.runs||[]).slice(0,8)) {
|
|
504
|
+
const runId = r.runId || r.id;
|
|
505
|
+
if (!runId) continue;
|
|
506
|
+
const status = String(r.status || r.state || '').toLowerCase();
|
|
507
|
+
const stage = String(r.stage || '').toLowerCase();
|
|
508
|
+
const lane = status.includes('complete') || status === 'done' || status === 'failed'
|
|
509
|
+
? 'done'
|
|
510
|
+
: stage.includes('hire')
|
|
511
|
+
? 'hiring'
|
|
512
|
+
: stage.includes('ghost') || stage.includes('review')
|
|
513
|
+
? 'ghostpass'
|
|
514
|
+
: status === 'running' || stage.includes('orchestrat')
|
|
515
|
+
? 'running'
|
|
516
|
+
: 'planning';
|
|
517
|
+
if (!cols[lane].some(c=>c.id===runId)) {
|
|
518
|
+
cols[lane].push({
|
|
519
|
+
id: runId,
|
|
520
|
+
goal: r.goal||r.mandate||'(run)',
|
|
521
|
+
status: status || stage || 'queued',
|
|
522
|
+
tokens:r.tokensUsed||r.tokenCount,
|
|
523
|
+
time:r.completedAt||r.updatedAt||r.createdAt||r.startedAt,
|
|
524
|
+
role:null,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
461
527
|
}
|
|
462
528
|
return cols;
|
|
463
529
|
}
|
|
@@ -89,17 +89,42 @@ function decisionRows(entries) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function renderRunRail(runs) {
|
|
92
|
-
if (!runs.length) return
|
|
92
|
+
if (!runs.length) return `<div class="empty">
|
|
93
|
+
<div class="empty-title">No runs recorded</div>
|
|
94
|
+
<div class="empty-sub">Run a goal from the command bar and its context spine will appear here.</div>
|
|
95
|
+
</div>`;
|
|
93
96
|
return runs.map(run => {
|
|
94
97
|
const id = runIdOf(run);
|
|
95
98
|
const active = id === S.contextLogSelectedRunId;
|
|
96
99
|
return `<button class="context-log-run ${active ? 'active' : ''}" data-context-run="${esc(id)}">
|
|
97
100
|
<span>${esc(String(id).slice(-10) || 'run')}</span>
|
|
98
|
-
<small>${esc(run.state || run.status || 'queued')} · ${esc(fmtTs(run.createdAt))}</small>
|
|
101
|
+
<small>${esc(run.stage || run.state || run.status || 'queued')} · ${esc(fmtTs(run.createdAt || run.startedAt || run.updatedAt))}</small>
|
|
99
102
|
</button>`;
|
|
100
103
|
}).join('');
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
function emptyContextMain() {
|
|
107
|
+
return `<div class="context-log-summary context-log-empty-state">
|
|
108
|
+
<div class="context-log-kpi"><span>Runs</span><strong>0</strong></div>
|
|
109
|
+
<div class="context-log-kpi"><span>Context</span><strong>—</strong></div>
|
|
110
|
+
<div class="context-log-kpi"><span>Decision</span><strong>—</strong></div>
|
|
111
|
+
<div class="context-log-selection">
|
|
112
|
+
<div>
|
|
113
|
+
<span>Start</span>
|
|
114
|
+
<button class="btn btn-sm" id="context-log-run-goal-btn">Run a goal</button>
|
|
115
|
+
</div>
|
|
116
|
+
<div>
|
|
117
|
+
<span>Expected here</span>
|
|
118
|
+
${chips(['request brief', 'selection plan', 'context events', 'decisions'])}
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<span>History</span>
|
|
122
|
+
<span class="context-log-empty">Previous runs persist after the dashboard restarts.</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>`;
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
export async function load() {
|
|
104
129
|
const runs = await api('/api/runs?limit=30', 3000);
|
|
105
130
|
S.contextLogRuns = Array.isArray(runs) ? runs : [];
|
|
@@ -130,7 +155,7 @@ export function render() {
|
|
|
130
155
|
${renderRunRail(S.contextLogRuns || [])}
|
|
131
156
|
</aside>
|
|
132
157
|
<section class="context-log-main">
|
|
133
|
-
${spine ? summaryCard(spine) :
|
|
158
|
+
${spine ? summaryCard(spine) : emptyContextMain()}
|
|
134
159
|
<div class="context-log-grid">
|
|
135
160
|
<div>
|
|
136
161
|
<div class="shd">Context events</div>
|
|
@@ -150,6 +175,10 @@ export function render() {
|
|
|
150
175
|
await load();
|
|
151
176
|
});
|
|
152
177
|
});
|
|
178
|
+
$('context-log-run-goal-btn')?.addEventListener('click', () => {
|
|
179
|
+
window.location.hash = '#board';
|
|
180
|
+
setTimeout(() => $('cmd-input')?.focus(), 100);
|
|
181
|
+
});
|
|
153
182
|
$('context-log-refresh-btn')?.addEventListener('click', async () => {
|
|
154
183
|
bustCache('/api/runs?limit=30');
|
|
155
184
|
if (S.contextLogSelectedRunId) bustCache(spineUrl(S.contextLogSelectedRunId));
|