@sapiom/harness 0.8.9 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/README.md +11 -1
- package/dist/core/agent-project-discovery.d.ts +235 -1
- package/dist/core/agent-project-discovery.d.ts.map +1 -1
- package/dist/core/agent-project-discovery.js +303 -1
- package/dist/core/agent-project-discovery.js.map +1 -1
- package/dist/core/canvas-interconnections.d.ts +31 -0
- package/dist/core/canvas-interconnections.d.ts.map +1 -1
- package/dist/core/canvas-interconnections.js +294 -21
- package/dist/core/canvas-interconnections.js.map +1 -1
- package/dist/core/canvas-render.d.ts +32 -0
- package/dist/core/canvas-render.d.ts.map +1 -1
- package/dist/core/canvas-render.js +50 -41
- package/dist/core/canvas-render.js.map +1 -1
- package/dist/core/definition-name.d.ts +12 -0
- package/dist/core/definition-name.d.ts.map +1 -1
- package/dist/core/definition-name.js +14 -5
- package/dist/core/definition-name.js.map +1 -1
- package/dist/core/system-graph-inventory.d.ts +68 -0
- package/dist/core/system-graph-inventory.d.ts.map +1 -0
- package/dist/core/system-graph-inventory.js +312 -0
- package/dist/core/system-graph-inventory.js.map +1 -0
- package/dist/core/system-graph-relationships.d.ts +41 -0
- package/dist/core/system-graph-relationships.d.ts.map +1 -0
- package/dist/core/system-graph-relationships.js +98 -0
- package/dist/core/system-graph-relationships.js.map +1 -0
- package/dist/core/system-graph-store.d.ts +45 -0
- package/dist/core/system-graph-store.d.ts.map +1 -0
- package/dist/core/system-graph-store.js +222 -0
- package/dist/core/system-graph-store.js.map +1 -0
- package/dist/core/system-graph-watcher.d.ts +51 -0
- package/dist/core/system-graph-watcher.d.ts.map +1 -0
- package/dist/core/system-graph-watcher.js +448 -0
- package/dist/core/system-graph-watcher.js.map +1 -0
- package/dist/core/system-graph.d.ts +43 -0
- package/dist/core/system-graph.d.ts.map +1 -0
- package/dist/core/system-graph.js +321 -0
- package/dist/core/system-graph.js.map +1 -0
- package/dist/core/workflow-registry.d.ts +84 -6
- package/dist/core/workflow-registry.d.ts.map +1 -1
- package/dist/core/workflow-registry.js +191 -91
- package/dist/core/workflow-registry.js.map +1 -1
- package/dist/core/workspace-watcher.d.ts +21 -13
- package/dist/core/workspace-watcher.d.ts.map +1 -1
- package/dist/core/workspace-watcher.js +79 -81
- package/dist/core/workspace-watcher.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server/agent-move.d.ts +134 -0
- package/dist/server/agent-move.d.ts.map +1 -0
- package/dist/server/agent-move.js +343 -0
- package/dist/server/agent-move.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +275 -22
- package/dist/server/index.js.map +1 -1
- package/dist/server/rest.d.ts +3 -0
- package/dist/server/rest.d.ts.map +1 -1
- package/dist/server/rest.js +3 -0
- package/dist/server/rest.js.map +1 -1
- package/dist/server/studio-rail.d.ts +76 -0
- package/dist/server/studio-rail.d.ts.map +1 -0
- package/dist/server/studio-rail.js +234 -0
- package/dist/server/studio-rail.js.map +1 -0
- package/dist/server/system-graph.d.ts +14 -0
- package/dist/server/system-graph.d.ts.map +1 -0
- package/dist/server/system-graph.js +40 -0
- package/dist/server/system-graph.js.map +1 -0
- package/dist/server/workflow-graph.d.ts +43 -0
- package/dist/server/workflow-graph.d.ts.map +1 -0
- package/dist/server/workflow-graph.js +200 -0
- package/dist/server/workflow-graph.js.map +1 -0
- package/dist/shared/system-graph.d.ts +59 -0
- package/dist/shared/system-graph.d.ts.map +1 -0
- package/dist/shared/system-graph.js +76 -0
- package/dist/shared/system-graph.js.map +1 -0
- package/dist/shared/types.d.ts +45 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/web/assets/index-BLrCZ-6M.css +32 -0
- package/dist/web/assets/index-CzCPIbq9.js +513 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/index-B18B2y5o.js +0 -494
- package/dist/web/assets/index-jGBbXx9R.css +0 -32
package/dist/server/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { createServer as createHttpServer, } from "node:http";
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
|
-
import { dirname, join, sep } from "node:path";
|
|
11
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import express from "express";
|
|
14
14
|
import { WebSocketServer } from "ws";
|
|
@@ -21,6 +21,7 @@ import { TaskManager } from "../core/task-manager.js";
|
|
|
21
21
|
import { createClaudeCodeAdapter } from "../core/adapters/claude-code.js";
|
|
22
22
|
import { createCodexAdapter } from "../core/adapters/codex.js";
|
|
23
23
|
import { WorkflowRegistry, createWorkflowsRouter, } from "../core/workflow-registry.js";
|
|
24
|
+
import { AgentProjectScanBudget } from "../core/agent-project-discovery.js";
|
|
24
25
|
import { DEFAULT_MACROS } from "../core/macros.js";
|
|
25
26
|
import { createEventStore } from "../core/collector/store.js";
|
|
26
27
|
import { createClaudeTranscriptEnricher, createSessionRecordReader, } from "../core/session-record.js";
|
|
@@ -36,7 +37,7 @@ import { findRolloutFile, tailCodexRollout, } from "../core/collector/codex-tail
|
|
|
36
37
|
import { getOrCreateMachineId } from "../cli/machine-id.js";
|
|
37
38
|
import { loadSettings, pruneDeadRecentDirs } from "../cli/settings.js";
|
|
38
39
|
import { generateClaudeSettings } from "../core/inject/claude-settings.js";
|
|
39
|
-
import { generateMcpConfig } from "../core/inject/mcp-config.js";
|
|
40
|
+
import { generateMcpConfig, } from "../core/inject/mcp-config.js";
|
|
40
41
|
import { generateSystemPromptFile } from "../core/inject/system-prompt.js";
|
|
41
42
|
import { generateSkillsPlugin } from "../core/inject/skills-plugin.js";
|
|
42
43
|
import { removeGeneratedSessionDir, sweepGeneratedDirs, } from "../core/inject/retention.js";
|
|
@@ -50,12 +51,17 @@ import { prepareHarnessContextForResume, writeHarnessContext, writeHarnessContex
|
|
|
50
51
|
import { ensureCanvasTemplate } from "../core/canvas-template.js";
|
|
51
52
|
import { renderCanvasForSession } from "../core/canvas-render.js";
|
|
52
53
|
import { invalidateExtractionCache } from "../core/canvas-cache.js";
|
|
54
|
+
import { CachedAgentRelationshipProvider, HarnessRegistryInventoryProvider, LocalWorkspaceScopeCatalog, SourceAgentRelationshipProvider, StaticSystemGraphBuilder, } from "../core/system-graph.js";
|
|
55
|
+
import { canonicalGraphPath, dirtyGraphSourceRoots, graphSourceRootsWithinScope, isWithinGraphPath, } from "../core/system-graph-inventory.js";
|
|
56
|
+
import { SystemGraphStore } from "../core/system-graph-store.js";
|
|
57
|
+
import { SystemGraphWatcherManager } from "../core/system-graph-watcher.js";
|
|
53
58
|
import { sweepNdjson } from "../core/collector/store-retention.js";
|
|
54
59
|
import { createDefinitionSlugResolver, resolveAgentsBaseUrl, } from "../core/definition-slug-resolver.js";
|
|
55
|
-
import { resolveManifestName } from "../core/definition-name.js";
|
|
60
|
+
import { inspectManifestName, resolveManifestName, } from "../core/definition-name.js";
|
|
56
61
|
import { createBootTokenMiddleware } from "./auth.js";
|
|
57
62
|
import { createApiKeyProvider } from "../core/api-key-provider.js";
|
|
58
63
|
import { createRestRouter } from "./rest.js";
|
|
64
|
+
import { createSystemGraphRouter } from "./system-graph.js";
|
|
59
65
|
import { createStaticRouter } from "./static.js";
|
|
60
66
|
import { createTerminalWebSocketHandler } from "./terminal-ws.js";
|
|
61
67
|
import { createEventsWebSocketHandler } from "./events-ws.js";
|
|
@@ -63,13 +69,16 @@ import { attachWebSocketRouters } from "./ws-router.js";
|
|
|
63
69
|
import { createIngestRouter, processIngest, } from "./ingest.js";
|
|
64
70
|
import { createCanvasRouter } from "./canvas.js";
|
|
65
71
|
import { createCanvasRenderRouter } from "./canvas-render.js";
|
|
72
|
+
import { createWorkflowGraphRouter } from "./workflow-graph.js";
|
|
73
|
+
import { createStudioRailRouter } from "./studio-rail.js";
|
|
74
|
+
import { createAgentMoveRouter, moveTargetDirs, remapSessions, } from "./agent-move.js";
|
|
66
75
|
import { createMacrosRouter } from "./macros.js";
|
|
67
76
|
import { createFsRouter } from "./fs.js";
|
|
68
77
|
import { createRunsRouter } from "./runs.js";
|
|
69
78
|
import { createTemplatesRouter } from "./templates.js";
|
|
70
79
|
import { createAccountRouter } from "./account.js";
|
|
71
80
|
import { createActionsRouter } from "./actions.js";
|
|
72
|
-
import { createAuthRouter, createMutableAuthState
|
|
81
|
+
import { createAuthRouter, createMutableAuthState } from "./auth-routes.js";
|
|
73
82
|
// resolveAgentsBaseUrl is imported above from definition-slug-resolver.js
|
|
74
83
|
// (an identical helper); the runs router reuses it for its agents base URL.
|
|
75
84
|
/**
|
|
@@ -102,6 +111,19 @@ const NDJSON_RETENTION_SWEEP_MS = 6 * 60 * 60 * 1000; // every 6 hours
|
|
|
102
111
|
function packageRoot() {
|
|
103
112
|
return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
104
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* One line per scan, on stderr with the rest of the harness's operational log.
|
|
116
|
+
* Deliberately unconditional rather than debug-gated: a scan happens a handful
|
|
117
|
+
* of times per session, and the one question this log answers ("what put that
|
|
118
|
+
* there?") is only ever asked after the fact.
|
|
119
|
+
*/
|
|
120
|
+
function logAgentScan(reason, root, found, budget) {
|
|
121
|
+
const truncated = budget.truncated
|
|
122
|
+
? `, TRUNCATED at depth ${String(budget.truncatedAtDepth)} (envelope ${budget.envelopeDepth})`
|
|
123
|
+
: "";
|
|
124
|
+
console.error(`[harness] agent scan (${reason}) ${root}: ${found} agent(s), ` +
|
|
125
|
+
`${budget.visited} dirs${truncated}`);
|
|
126
|
+
}
|
|
105
127
|
/** Whether two workflow lists are equivalent for the rail's purposes —
|
|
106
128
|
* compares the fields the SPA actually renders/keys on, order-insensitive, so
|
|
107
129
|
* a rescan that turned up nothing new doesn't trigger a needless broadcast. */
|
|
@@ -164,7 +186,11 @@ function createDefaultBuildLaunchOpts(apiKey, generatedRoot, rehydration, sapiom
|
|
|
164
186
|
// dim text loses contrast. Only when the theme is known: an unthemed path
|
|
165
187
|
// (server-side auto-create, a legacy session resumed before this existed)
|
|
166
188
|
// omits it and Claude keeps its default 256-color rendering, exactly as before.
|
|
167
|
-
const claudeTheme = req.theme === "light"
|
|
189
|
+
const claudeTheme = req.theme === "light"
|
|
190
|
+
? "light-ansi"
|
|
191
|
+
: req.theme === "dark"
|
|
192
|
+
? "dark-ansi"
|
|
193
|
+
: undefined;
|
|
168
194
|
const [settings, mcpConfigFile, systemPromptFile, pluginDir] = await Promise.all([
|
|
169
195
|
generateClaudeSettings({
|
|
170
196
|
harnessSessionId,
|
|
@@ -191,7 +217,9 @@ function createDefaultBuildLaunchOpts(apiKey, generatedRoot, rehydration, sapiom
|
|
|
191
217
|
...(pluginDir ? { pluginDir } : {}),
|
|
192
218
|
// Set on BOTH channels: the post-ready path hasn't delivered yet, but a
|
|
193
219
|
// brief exists and will, and this is the flag that tells it to.
|
|
194
|
-
...(brief !== null && rehydrateFrom
|
|
220
|
+
...(brief !== null && rehydrateFrom
|
|
221
|
+
? { rehydratedFrom: rehydrateFrom }
|
|
222
|
+
: {}),
|
|
195
223
|
};
|
|
196
224
|
};
|
|
197
225
|
}
|
|
@@ -439,7 +467,8 @@ export const startServer = async (options) => {
|
|
|
439
467
|
return null;
|
|
440
468
|
try {
|
|
441
469
|
const rows = await adapter.listPastSessions(record.cwd);
|
|
442
|
-
return rows.find((row) => row.agentSessionId === record.agentSessionId)
|
|
470
|
+
return (rows.find((row) => row.agentSessionId === record.agentSessionId)
|
|
471
|
+
?.gitBranch ?? null);
|
|
443
472
|
}
|
|
444
473
|
catch {
|
|
445
474
|
return null;
|
|
@@ -472,7 +501,11 @@ export const startServer = async (options) => {
|
|
|
472
501
|
title: prior?.title ?? null,
|
|
473
502
|
gitBranch: await priorGitBranch(record),
|
|
474
503
|
workflow: workflow
|
|
475
|
-
? {
|
|
504
|
+
? {
|
|
505
|
+
name: workflow.name,
|
|
506
|
+
path: workflow.path,
|
|
507
|
+
definitionId: workflow.definitionId,
|
|
508
|
+
}
|
|
476
509
|
: null,
|
|
477
510
|
};
|
|
478
511
|
},
|
|
@@ -500,6 +533,43 @@ export const startServer = async (options) => {
|
|
|
500
533
|
ensureCanvasTemplate,
|
|
501
534
|
});
|
|
502
535
|
await sessionManager.init();
|
|
536
|
+
const workspaceScopeCatalog = new LocalWorkspaceScopeCatalog(async () => [
|
|
537
|
+
...(await loadSettings(statePaths.settings)).recentDirs,
|
|
538
|
+
...sessionManager.list().map((session) => session.cwd),
|
|
539
|
+
]);
|
|
540
|
+
const systemGraphRelationships = new CachedAgentRelationshipProvider(new SourceAgentRelationshipProvider());
|
|
541
|
+
const systemGraphStore = new SystemGraphStore(new StaticSystemGraphBuilder(new HarnessRegistryInventoryProvider({
|
|
542
|
+
listWorkflows: () => workflowsCache,
|
|
543
|
+
inspectManifestName,
|
|
544
|
+
}), systemGraphRelationships), {
|
|
545
|
+
onChange: ({ workspaceKey, revision, state }) => {
|
|
546
|
+
bus.publish({
|
|
547
|
+
type: "system-graph.changed",
|
|
548
|
+
workspaceKey,
|
|
549
|
+
revision,
|
|
550
|
+
state,
|
|
551
|
+
});
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
const activeSystemGraphScopes = new Map();
|
|
555
|
+
const refreshSystemGraphScopesForRoot = (changedRoot, excludedWorkspaceKey) => {
|
|
556
|
+
const canonicalChangedRoot = canonicalGraphPath(changedRoot);
|
|
557
|
+
for (const scope of activeSystemGraphScopes.values()) {
|
|
558
|
+
if (scope.workspaceKey === excludedWorkspaceKey)
|
|
559
|
+
continue;
|
|
560
|
+
if (!systemGraphStore.peek(scope.workspaceKey))
|
|
561
|
+
continue;
|
|
562
|
+
const scopeRoot = canonicalGraphPath(scope.root);
|
|
563
|
+
if (!isWithinGraphPath(scopeRoot, canonicalChangedRoot) &&
|
|
564
|
+
!isWithinGraphPath(canonicalChangedRoot, scopeRoot)) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
systemGraphStore.requestRefresh({
|
|
568
|
+
workspaceKey: scope.workspaceKey,
|
|
569
|
+
root: scopeRoot,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
};
|
|
503
573
|
const sessionSweepTimer = setInterval(() => sessionManager.sweepDeadSessions(), SESSION_LIVENESS_SWEEP_MS);
|
|
504
574
|
sessionSweepTimer.unref?.();
|
|
505
575
|
// Portable continue, second channel: for a harness whose adapter never puts
|
|
@@ -518,7 +588,8 @@ export const startServer = async (options) => {
|
|
|
518
588
|
const rehydratedFrom = session.rehydratedFrom;
|
|
519
589
|
if (!session.ready || !rehydratedFrom)
|
|
520
590
|
return;
|
|
521
|
-
if (systemPromptDeliveryFor(adapters[session.harness]) !==
|
|
591
|
+
if (systemPromptDeliveryFor(adapters[session.harness]) !==
|
|
592
|
+
"post-ready-injection")
|
|
522
593
|
return;
|
|
523
594
|
if (briefsDelivered.has(session.id))
|
|
524
595
|
return;
|
|
@@ -571,7 +642,9 @@ export const startServer = async (options) => {
|
|
|
571
642
|
readRecord: (harnessSessionId) => sessionRecordReader.read(harnessSessionId),
|
|
572
643
|
getSession: (harnessSessionId) => {
|
|
573
644
|
const session = sessionManager.get(harnessSessionId);
|
|
574
|
-
return session
|
|
645
|
+
return session
|
|
646
|
+
? { harness: session.harness, cwd: session.cwd }
|
|
647
|
+
: undefined;
|
|
575
648
|
},
|
|
576
649
|
runTask: (req) => taskManager.run(req),
|
|
577
650
|
});
|
|
@@ -606,9 +679,15 @@ export const startServer = async (options) => {
|
|
|
606
679
|
return;
|
|
607
680
|
const before = workflowsCache;
|
|
608
681
|
await workflowRegistry.prune();
|
|
609
|
-
|
|
682
|
+
const rescanBudget = new AgentProjectScanBudget();
|
|
683
|
+
const rescanFound = await workflowRegistry.scan(session.cwd, rescanBudget);
|
|
684
|
+
logAgentScan("workspace-change", session.cwd, rescanFound.length, rescanBudget);
|
|
610
685
|
const after = await workflowRegistry.list();
|
|
611
686
|
workflowsCache = after;
|
|
687
|
+
const inventoryChanged = !workflowListsEqual(before, after);
|
|
688
|
+
if (inventoryChanged) {
|
|
689
|
+
await refreshSystemGraphScopesForRoot(session.cwd);
|
|
690
|
+
}
|
|
612
691
|
// A removed project must not leave a live session permanently bound to a
|
|
613
692
|
// path that the registry can no longer resolve. Clear only stale bindings;
|
|
614
693
|
// the triggering session may immediately auto-bind to another candidate
|
|
@@ -646,7 +725,7 @@ export const startServer = async (options) => {
|
|
|
646
725
|
});
|
|
647
726
|
}
|
|
648
727
|
}
|
|
649
|
-
if (
|
|
728
|
+
if (!inventoryChanged)
|
|
650
729
|
return;
|
|
651
730
|
await Promise.all(sessionManager.list().map((s) => writeSessionContext(s)));
|
|
652
731
|
bus.publish({ type: "workflows.changed" });
|
|
@@ -773,17 +852,92 @@ export const startServer = async (options) => {
|
|
|
773
852
|
// merged registry) — callers use this to decide whether THIS scan turned
|
|
774
853
|
// up something new worth an unprompted canvas render, without conflating
|
|
775
854
|
// it with unrelated workflows some earlier scan already found elsewhere.
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
855
|
+
//
|
|
856
|
+
// Every call names WHY it is scanning. The registry is the thing the user
|
|
857
|
+
// asked "how am I finding my agents?" about, and until this line existed the
|
|
858
|
+
// only honest answer available after the fact was "something scanned
|
|
859
|
+
// something". One install ended up with 88 agents across twelve repositories
|
|
860
|
+
// nobody had opened, and reconstructing which root did it took a filesystem
|
|
861
|
+
// archaeology session. Now every scan says its root, its reason, what it
|
|
862
|
+
// found, what it cost, and what it declined to enter.
|
|
863
|
+
const scanWorkflowsAndBroadcast = async (root, reason, options = {}) => {
|
|
864
|
+
const before = workflowsCache;
|
|
865
|
+
const budget = new AgentProjectScanBudget();
|
|
866
|
+
const found = await workflowRegistry.scan(root, budget);
|
|
867
|
+
logAgentScan(reason, root, found.length, budget);
|
|
868
|
+
const after = await workflowRegistry.list();
|
|
869
|
+
workflowsCache = after;
|
|
870
|
+
const changed = !workflowListsEqual(before, after);
|
|
779
871
|
// Rewrite every open session's context file before broadcasting — a
|
|
780
872
|
// listener reacting to workflows.changed (the SPA, or an agent that
|
|
781
873
|
// happens to re-read the file right then) must never see the
|
|
782
874
|
// notification before the file it describes is actually updated.
|
|
783
|
-
|
|
784
|
-
|
|
875
|
+
if (changed) {
|
|
876
|
+
await Promise.all(sessionManager.list().map((session) => writeSessionContext(session)));
|
|
877
|
+
}
|
|
878
|
+
if (options.refreshGraphs)
|
|
879
|
+
await options.refreshGraphs();
|
|
880
|
+
else if (changed)
|
|
881
|
+
refreshSystemGraphScopesForRoot(root);
|
|
882
|
+
if (changed)
|
|
883
|
+
bus.publish({ type: "workflows.changed" });
|
|
785
884
|
return found;
|
|
786
885
|
};
|
|
886
|
+
const refreshSystemGraphInventory = async (scope) => {
|
|
887
|
+
const canonicalScope = {
|
|
888
|
+
workspaceKey: scope.workspaceKey,
|
|
889
|
+
root: canonicalGraphPath(scope.root),
|
|
890
|
+
};
|
|
891
|
+
let refreshPromise = null;
|
|
892
|
+
const refreshGraph = () => {
|
|
893
|
+
refreshSystemGraphScopesForRoot(canonicalScope.root, canonicalScope.workspaceKey);
|
|
894
|
+
refreshPromise = systemGraphStore.refresh(canonicalScope);
|
|
895
|
+
return refreshPromise.then(() => undefined);
|
|
896
|
+
};
|
|
897
|
+
await scanWorkflowsAndBroadcast(canonicalScope.root, "graph-refresh", {
|
|
898
|
+
refreshGraphs: refreshGraph,
|
|
899
|
+
});
|
|
900
|
+
return await (refreshPromise ?? systemGraphStore.refresh(canonicalScope));
|
|
901
|
+
};
|
|
902
|
+
const workflowRootsForGraphScope = (scope) => graphSourceRootsWithinScope(scope.root, workflowsCache.map((workflow) => workflow.path));
|
|
903
|
+
const systemGraphWatcher = new SystemGraphWatcherManager({
|
|
904
|
+
listSourceRoots: workflowRootsForGraphScope,
|
|
905
|
+
onSourceChange: (scope, sourcePaths) => {
|
|
906
|
+
const canonicalScope = {
|
|
907
|
+
workspaceKey: scope.workspaceKey,
|
|
908
|
+
root: canonicalGraphPath(scope.root),
|
|
909
|
+
};
|
|
910
|
+
const dirtyRoots = dirtyGraphSourceRoots(canonicalScope.root, workflowsCache.map((workflow) => workflow.path), sourcePaths);
|
|
911
|
+
if (dirtyRoots.length === 0)
|
|
912
|
+
return;
|
|
913
|
+
for (const workflowRoot of dirtyRoots) {
|
|
914
|
+
systemGraphRelationships.invalidateSource(workflowRoot);
|
|
915
|
+
}
|
|
916
|
+
systemGraphStore.requestRefresh(canonicalScope);
|
|
917
|
+
},
|
|
918
|
+
onInventoryChange: async (scope) => {
|
|
919
|
+
try {
|
|
920
|
+
await refreshSystemGraphInventory(scope);
|
|
921
|
+
}
|
|
922
|
+
catch (err) {
|
|
923
|
+
systemGraphStore.reportRefreshFailure(scope);
|
|
924
|
+
console.error("[harness] workspace graph inventory refresh failed");
|
|
925
|
+
throw err;
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
const listWorkspaceScopesAndRetain = async () => {
|
|
930
|
+
const scopes = await workspaceScopeCatalog.list();
|
|
931
|
+
const retained = new Set(scopes.map((scope) => scope.workspaceKey));
|
|
932
|
+
systemGraphWatcher.retain(retained);
|
|
933
|
+
systemGraphStore.retain(retained);
|
|
934
|
+
for (const workspaceKey of activeSystemGraphScopes.keys()) {
|
|
935
|
+
if (!retained.has(workspaceKey)) {
|
|
936
|
+
activeSystemGraphScopes.delete(workspaceKey);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return scopes;
|
|
940
|
+
};
|
|
787
941
|
/** Enrich only the bound workflow before a Canvas render. Canvas extraction
|
|
788
942
|
* needs the registry snapshot to resolve the binding, but its cloud badge
|
|
789
943
|
* needs the same mutable build projection exposed by /api/state. Limiting
|
|
@@ -838,7 +992,7 @@ export const startServer = async (options) => {
|
|
|
838
992
|
const outcome = await renderCanvasForSession(session, await canvasWorkflowsForSession(session), { surfaceErrorOnMissingDeps: true });
|
|
839
993
|
reactToRenderOutcome(session, outcome);
|
|
840
994
|
};
|
|
841
|
-
const initialWorkflowScan = scanWorkflowsAndBroadcast(launchDir).catch((err) => {
|
|
995
|
+
const initialWorkflowScan = scanWorkflowsAndBroadcast(launchDir, "boot").catch((err) => {
|
|
842
996
|
console.error("[harness] initial agent scan failed:", err);
|
|
843
997
|
return [];
|
|
844
998
|
});
|
|
@@ -941,13 +1095,14 @@ export const startServer = async (options) => {
|
|
|
941
1095
|
}
|
|
942
1096
|
: null,
|
|
943
1097
|
listWorkflows: () => workflowRegistry.list().then(enrichWorkflows),
|
|
1098
|
+
listWorkspaceScopes: listWorkspaceScopesAndRetain,
|
|
944
1099
|
listMacros: () => DEFAULT_MACROS,
|
|
945
1100
|
findWorkflow: (workflowPath) => workflowsCache.find((w) => w.path === workflowPath) ?? null,
|
|
946
1101
|
writeWorkspaceContext: writeSessionContext,
|
|
947
1102
|
renderCanvas,
|
|
948
1103
|
onTelemetryOptInChange: (optIn) => batcher.setTelemetryOptIn(optIn),
|
|
949
1104
|
onSessionCreated: (cwd, harnessSessionId) => {
|
|
950
|
-
scanWorkflowsAndBroadcast(cwd)
|
|
1105
|
+
scanWorkflowsAndBroadcast(cwd, "session-create")
|
|
951
1106
|
.then((found) => {
|
|
952
1107
|
// Only auto-render when THIS session's own directory turned up a
|
|
953
1108
|
// workflow — an unrelated project scanned earlier elsewhere in
|
|
@@ -982,6 +1137,23 @@ export const startServer = async (options) => {
|
|
|
982
1137
|
},
|
|
983
1138
|
settingsPath: statePaths.settings,
|
|
984
1139
|
}));
|
|
1140
|
+
app.use("/api", createSystemGraphRouter({
|
|
1141
|
+
scopeResolver: workspaceScopeCatalog,
|
|
1142
|
+
store: systemGraphStore,
|
|
1143
|
+
onScopeAccess: (scope) => {
|
|
1144
|
+
activeSystemGraphScopes.set(scope.workspaceKey, scope);
|
|
1145
|
+
return systemGraphWatcher.start(scope);
|
|
1146
|
+
},
|
|
1147
|
+
onScopeRefresh: async (scope) => {
|
|
1148
|
+
try {
|
|
1149
|
+
return await refreshSystemGraphInventory(scope);
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
console.error("[harness] workspace graph manual refresh failed");
|
|
1153
|
+
return systemGraphStore.reportRefreshFailure(scope);
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
}));
|
|
985
1157
|
app.use("/api", createCanvasRenderRouter({
|
|
986
1158
|
getSession: (harnessSessionId) => sessionManager.get(harnessSessionId),
|
|
987
1159
|
listWorkflows: canvasWorkflowsForSession,
|
|
@@ -1000,8 +1172,23 @@ export const startServer = async (options) => {
|
|
|
1000
1172
|
// as WorkflowRegistryLike so this wrapper needs no unsafe cast.
|
|
1001
1173
|
const enrichedWorkflowRegistry = {
|
|
1002
1174
|
list: () => workflowRegistry.list().then(enrichWorkflows),
|
|
1003
|
-
scan: (root) =>
|
|
1004
|
-
connectPath: (inputPath) =>
|
|
1175
|
+
scan: (root) => scanWorkflowsAndBroadcast(root, "requested"),
|
|
1176
|
+
connectPath: async (inputPath) => {
|
|
1177
|
+
const workflow = await workflowRegistry.connectPath(inputPath);
|
|
1178
|
+
workflowsCache = await workflowRegistry.list();
|
|
1179
|
+
await Promise.all(sessionManager.list().map((session) => writeSessionContext(session)));
|
|
1180
|
+
await refreshSystemGraphScopesForRoot(workflow.path);
|
|
1181
|
+
bus.publish({ type: "workflows.changed" });
|
|
1182
|
+
return workflow;
|
|
1183
|
+
},
|
|
1184
|
+
scanWithBoundaries: async (root) => {
|
|
1185
|
+
const budget = new AgentProjectScanBudget();
|
|
1186
|
+
const found = await workflowRegistry.scan(root, budget);
|
|
1187
|
+
logAgentScan("requested", root, found.length, budget);
|
|
1188
|
+
// The boundaries the walk stopped at travel with the result so the rail
|
|
1189
|
+
// can explain an empty project instead of misdescribing one.
|
|
1190
|
+
return { found, repositoryBoundaries: budget.repositoryBoundaries };
|
|
1191
|
+
},
|
|
1005
1192
|
};
|
|
1006
1193
|
app.use(createRunsRouter({
|
|
1007
1194
|
// Pass the provider (not a static key) so the live-status path can refresh
|
|
@@ -1044,7 +1231,69 @@ export const startServer = async (options) => {
|
|
|
1044
1231
|
// which the watcher's directory-set diff cannot see — rescan that project
|
|
1045
1232
|
// so the Draft→Deployed chip and the deploy-gated actions update.
|
|
1046
1233
|
onLinked: async (workflow) => {
|
|
1047
|
-
await scanWorkflowsAndBroadcast(workflow.path);
|
|
1234
|
+
await scanWorkflowsAndBroadcast(workflow.path, "agent-linked");
|
|
1235
|
+
},
|
|
1236
|
+
}));
|
|
1237
|
+
// IA-01: the session-free, workflow-keyed canvas route. Same derivation the
|
|
1238
|
+
// session-bound render uses, keyed by the agent's absolute path instead of a
|
|
1239
|
+
// session id — so a board can be read for an agent that has never hosted a
|
|
1240
|
+
// session. Resolution goes through the same live cache actions.ts uses, so
|
|
1241
|
+
// only registered agents are ever read from disk.
|
|
1242
|
+
app.use(createWorkflowGraphRouter({
|
|
1243
|
+
resolveWorkflow: (agentPath) => workflowsCache.find((w) => resolve(w.path) === agentPath) ?? null,
|
|
1244
|
+
}));
|
|
1245
|
+
// SAP-2929: the Group axis's stored arrangement, one `.sapiom/studio-rail.json`
|
|
1246
|
+
// per project root, plus the launch edges it seeds from. Writable roots are
|
|
1247
|
+
// exactly the roots the rail can SHOW — recentDirs, the configured project
|
|
1248
|
+
// root, and live session cwds — so the route cannot be aimed anywhere the
|
|
1249
|
+
// studio has not already been pointed.
|
|
1250
|
+
app.use(createStudioRailRouter({
|
|
1251
|
+
listKnownRoots: async () => {
|
|
1252
|
+
const stored = await loadSettings(statePaths.settings);
|
|
1253
|
+
return [
|
|
1254
|
+
...stored.recentDirs,
|
|
1255
|
+
...(stored.projectRoot ? [stored.projectRoot] : []),
|
|
1256
|
+
...sessionManager.list().map((session) => session.cwd),
|
|
1257
|
+
];
|
|
1258
|
+
},
|
|
1259
|
+
listWorkflows: () => workflowsCache,
|
|
1260
|
+
}));
|
|
1261
|
+
// SAP-2930: the Project axis's drag, performed on disk. Its own guards live
|
|
1262
|
+
// in the module — a planner is not a permission system, so the route stats
|
|
1263
|
+
// the destination itself. Only a REGISTERED agent may be moved, resolved
|
|
1264
|
+
// through the same live cache the canvas and action routes use, and it may
|
|
1265
|
+
// only be moved INTO a directory the rail can show: the roots studio-rail
|
|
1266
|
+
// already treats as writable, plus the branching directories the Project
|
|
1267
|
+
// axis draws between a root and an agent. Both sides of the move are
|
|
1268
|
+
// therefore server-authored paths, not strings off the request.
|
|
1269
|
+
app.use(createAgentMoveRouter({
|
|
1270
|
+
resolveAgent: (agentPath) => workflowsCache.find((w) => resolve(w.path) === agentPath) ?? null,
|
|
1271
|
+
listMoveTargetDirs: async () => {
|
|
1272
|
+
const stored = await loadSettings(statePaths.settings);
|
|
1273
|
+
return moveTargetDirs([
|
|
1274
|
+
...stored.recentDirs,
|
|
1275
|
+
...(stored.projectRoot ? [stored.projectRoot] : []),
|
|
1276
|
+
...sessionManager.list().map((session) => session.cwd),
|
|
1277
|
+
], workflowsCache.map((w) => w.path));
|
|
1278
|
+
},
|
|
1279
|
+
// Everything under the moved directory travelled with it, so every live
|
|
1280
|
+
// session whose cwd sat inside follows — a session left pointing at a
|
|
1281
|
+
// directory that no longer exists is the whole reason this remap exists.
|
|
1282
|
+
// Then prune the path that is gone and rescan the destination, which
|
|
1283
|
+
// broadcasts `workflows.changed`: the rail re-derives the tree from the
|
|
1284
|
+
// NEW path rather than from a stale registry row.
|
|
1285
|
+
onMoved: async (from, to) => {
|
|
1286
|
+
remapSessions(sessionManager.list(), from, to);
|
|
1287
|
+
await workflowRegistry.prune();
|
|
1288
|
+
await scanWorkflowsAndBroadcast(dirname(to), "agent-moved", {
|
|
1289
|
+
refreshGraphs: async () => {
|
|
1290
|
+
// A cross-project move changes two containment inventories. Refresh
|
|
1291
|
+
// every active parent/nested graph touching either side so neither
|
|
1292
|
+
// keeps a ghost node or misses the arrival.
|
|
1293
|
+
refreshSystemGraphScopesForRoot(dirname(from));
|
|
1294
|
+
refreshSystemGraphScopesForRoot(dirname(to));
|
|
1295
|
+
},
|
|
1296
|
+
});
|
|
1048
1297
|
},
|
|
1049
1298
|
}));
|
|
1050
1299
|
app.use(createWorkflowsRouter(enrichedWorkflowRegistry), createFsRouter(), createMacrosRouter({
|
|
@@ -1320,6 +1569,10 @@ export const startServer = async (options) => {
|
|
|
1320
1569
|
clearInterval(ndjsonRetentionTimer);
|
|
1321
1570
|
canvasWatcher.stopAll();
|
|
1322
1571
|
workspaceWatcher.stopAll();
|
|
1572
|
+
systemGraphWatcher.stopAll();
|
|
1573
|
+
activeSystemGraphScopes.clear();
|
|
1574
|
+
systemGraphRelationships.clear();
|
|
1575
|
+
systemGraphStore.clear();
|
|
1323
1576
|
installWatcher.stopAll();
|
|
1324
1577
|
for (const tailer of codexTailers.values())
|
|
1325
1578
|
tailer.stop();
|