@soleri/core 9.5.0 → 9.6.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/dist/adapters/claude-code-adapter.d.ts +27 -0
- package/dist/adapters/claude-code-adapter.d.ts.map +1 -0
- package/dist/adapters/claude-code-adapter.js +111 -0
- package/dist/adapters/claude-code-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/registry.d.ts +21 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +44 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/types.d.ts +93 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +10 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/brain/brain.d.ts +12 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +106 -44
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +36 -30
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/chat/agent-loop.js +1 -1
- package/dist/chat/agent-loop.js.map +1 -1
- package/dist/chat/notifications.d.ts.map +1 -1
- package/dist/chat/notifications.js +4 -0
- package/dist/chat/notifications.js.map +1 -1
- package/dist/control/intent-router.d.ts +1 -0
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +11 -5
- package/dist/control/intent-router.js.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +141 -27
- package/dist/curator/curator.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +1 -0
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/packs/index.d.ts +3 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +3 -2
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +23 -1
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js +50 -4
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/pack-installer.d.ts +10 -0
- package/dist/packs/pack-installer.d.ts.map +1 -1
- package/dist/packs/pack-installer.js +69 -2
- package/dist/packs/pack-installer.js.map +1 -1
- package/dist/packs/pack-lifecycle.d.ts +50 -0
- package/dist/packs/pack-lifecycle.d.ts.map +1 -0
- package/dist/packs/pack-lifecycle.js +91 -0
- package/dist/packs/pack-lifecycle.js.map +1 -0
- package/dist/packs/types.d.ts +64 -44
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +9 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/persistence/sqlite-provider.d.ts +5 -1
- package/dist/persistence/sqlite-provider.d.ts.map +1 -1
- package/dist/persistence/sqlite-provider.js +22 -2
- package/dist/persistence/sqlite-provider.js.map +1 -1
- package/dist/planning/github-projection.d.ts +8 -8
- package/dist/planning/github-projection.d.ts.map +1 -1
- package/dist/planning/github-projection.js +42 -42
- package/dist/planning/github-projection.js.map +1 -1
- package/dist/plugins/types.d.ts +21 -21
- package/dist/queue/pipeline-runner.d.ts.map +1 -1
- package/dist/queue/pipeline-runner.js +4 -0
- package/dist/queue/pipeline-runner.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +9 -1
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +169 -0
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +133 -4
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +128 -90
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +44 -11
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/shutdown-registry.d.ts +36 -0
- package/dist/runtime/shutdown-registry.d.ts.map +1 -0
- package/dist/runtime/shutdown-registry.js +74 -0
- package/dist/runtime/shutdown-registry.js.map +1 -0
- package/dist/runtime/types.d.ts +10 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/subagent/concurrency-manager.d.ts +29 -0
- package/dist/subagent/concurrency-manager.d.ts.map +1 -0
- package/dist/subagent/concurrency-manager.js +73 -0
- package/dist/subagent/concurrency-manager.js.map +1 -0
- package/dist/subagent/dispatcher.d.ts +41 -0
- package/dist/subagent/dispatcher.d.ts.map +1 -0
- package/dist/subagent/dispatcher.js +259 -0
- package/dist/subagent/dispatcher.js.map +1 -0
- package/dist/subagent/index.d.ts +14 -0
- package/dist/subagent/index.d.ts.map +1 -0
- package/dist/subagent/index.js +15 -0
- package/dist/subagent/index.js.map +1 -0
- package/dist/subagent/orphan-reaper.d.ts +37 -0
- package/dist/subagent/orphan-reaper.d.ts.map +1 -0
- package/dist/subagent/orphan-reaper.js +71 -0
- package/dist/subagent/orphan-reaper.js.map +1 -0
- package/dist/subagent/result-aggregator.d.ts +7 -0
- package/dist/subagent/result-aggregator.d.ts.map +1 -0
- package/dist/subagent/result-aggregator.js +57 -0
- package/dist/subagent/result-aggregator.js.map +1 -0
- package/dist/subagent/task-checkout.d.ts +36 -0
- package/dist/subagent/task-checkout.d.ts.map +1 -0
- package/dist/subagent/task-checkout.js +52 -0
- package/dist/subagent/task-checkout.js.map +1 -0
- package/dist/subagent/types.d.ts +114 -0
- package/dist/subagent/types.d.ts.map +1 -0
- package/dist/subagent/types.js +9 -0
- package/dist/subagent/types.js.map +1 -0
- package/dist/subagent/workspace-resolver.d.ts +35 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -0
- package/dist/subagent/workspace-resolver.js +99 -0
- package/dist/subagent/workspace-resolver.js.map +1 -0
- package/dist/transport/http-server.d.ts.map +1 -1
- package/dist/transport/http-server.js +49 -3
- package/dist/transport/http-server.js.map +1 -1
- package/dist/transport/ws-server.d.ts.map +1 -1
- package/dist/transport/ws-server.js +7 -0
- package/dist/transport/ws-server.js.map +1 -1
- package/dist/vault/linking.d.ts +3 -4
- package/dist/vault/linking.d.ts.map +1 -1
- package/dist/vault/linking.js +79 -32
- package/dist/vault/linking.js.map +1 -1
- package/dist/vault/vault-maintenance.d.ts.map +1 -1
- package/dist/vault/vault-maintenance.js +7 -14
- package/dist/vault/vault-maintenance.js.map +1 -1
- package/dist/vault/vault-memories.d.ts.map +1 -1
- package/dist/vault/vault-memories.js +19 -9
- package/dist/vault/vault-memories.js.map +1 -1
- package/dist/vault/vault-schema.d.ts +1 -0
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +20 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +7 -3
- package/dist/vault/vault.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/adapters/claude-code-adapter.test.ts +167 -0
- package/src/__tests__/adapters/registry.test.ts +100 -0
- package/src/__tests__/packs/pack-lifecycle.test.ts +379 -0
- package/src/__tests__/subagent/concurrency-manager.test.ts +132 -0
- package/src/__tests__/subagent/dispatcher.test.ts +195 -0
- package/src/__tests__/subagent/orphan-reaper.test.ts +141 -0
- package/src/__tests__/subagent/result-aggregator.test.ts +141 -0
- package/src/__tests__/subagent/task-checkout.test.ts +86 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +138 -0
- package/src/adapters/claude-code-adapter.ts +163 -0
- package/src/adapters/index.ts +22 -0
- package/src/adapters/registry.ts +53 -0
- package/src/adapters/types.ts +114 -0
- package/src/curator/curator.ts +1 -0
- package/src/index.ts +38 -1
- package/src/packs/index.ts +5 -1
- package/src/packs/lockfile.ts +70 -5
- package/src/packs/pack-installer.ts +78 -2
- package/src/packs/pack-lifecycle.ts +115 -0
- package/src/packs/pack-lockfile.test.ts +1 -1
- package/src/packs/pack-system.test.ts +1 -1
- package/src/packs/types.ts +40 -2
- package/src/persistence/sqlite-provider.ts +26 -2
- package/src/runtime/admin-setup-ops.test.ts +9 -4
- package/src/runtime/orchestrate-ops.ts +153 -1
- package/src/runtime/runtime.ts +15 -0
- package/src/runtime/session-briefing.test.ts +94 -2
- package/src/runtime/session-briefing.ts +48 -12
- package/src/runtime/types.ts +6 -0
- package/src/subagent/concurrency-manager.ts +89 -0
- package/src/subagent/dispatcher.ts +326 -0
- package/src/subagent/index.ts +28 -0
- package/src/subagent/orphan-reaper.ts +82 -0
- package/src/subagent/result-aggregator.ts +66 -0
- package/src/subagent/task-checkout.ts +60 -0
- package/src/subagent/types.ts +138 -0
- package/src/subagent/workspace-resolver.ts +117 -0
- package/src/vault/vault-scaling.test.ts +3 -2
- package/vitest.config.ts +2 -0
- package/src/hooks/index.ts +0 -6
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrphanReaper — tracks spawned child processes and detects dead/orphaned ones.
|
|
3
|
+
*
|
|
4
|
+
* Uses `process.kill(pid, 0)` (signal 0) as an existence check:
|
|
5
|
+
* - No error → process is alive
|
|
6
|
+
* - ESRCH → process is dead (reap it)
|
|
7
|
+
* - EPERM → process is alive but we lack permission to signal it
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { TrackedProcess } from './types.js';
|
|
11
|
+
|
|
12
|
+
export class OrphanReaper {
|
|
13
|
+
private readonly tracked = new Map<number, TrackedProcess>();
|
|
14
|
+
private readonly onOrphan?: (taskId: string, pid: number) => void;
|
|
15
|
+
|
|
16
|
+
constructor(onOrphan?: (taskId: string, pid: number) => void) {
|
|
17
|
+
this.onOrphan = onOrphan;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Start tracking a process. */
|
|
21
|
+
register(pid: number, taskId: string): void {
|
|
22
|
+
this.tracked.set(pid, { pid, taskId, registeredAt: Date.now() });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Stop tracking a process (called on normal completion). */
|
|
26
|
+
unregister(pid: number): void {
|
|
27
|
+
this.tracked.delete(pid);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check each tracked PID for liveness. Dead processes are removed from
|
|
32
|
+
* tracking, the onOrphan callback is invoked, and they are returned.
|
|
33
|
+
*/
|
|
34
|
+
reap(): TrackedProcess[] {
|
|
35
|
+
const reaped: TrackedProcess[] = [];
|
|
36
|
+
|
|
37
|
+
for (const [pid, entry] of this.tracked) {
|
|
38
|
+
if (!this.isAlive(pid)) {
|
|
39
|
+
this.tracked.delete(pid);
|
|
40
|
+
this.onOrphan?.(entry.taskId, pid);
|
|
41
|
+
reaped.push(entry);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return reaped;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Return all currently tracked processes. */
|
|
49
|
+
listTracked(): TrackedProcess[] {
|
|
50
|
+
return [...this.tracked.values()];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Check if a PID is currently tracked. */
|
|
54
|
+
isTracked(pid: number): boolean {
|
|
55
|
+
return this.tracked.has(pid);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Clear all tracked processes without killing them. */
|
|
59
|
+
clear(): void {
|
|
60
|
+
this.tracked.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── internals ──────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Signal-0 existence check.
|
|
67
|
+
* - No error → alive
|
|
68
|
+
* - EPERM → alive (exists but we can't signal it)
|
|
69
|
+
* - ESRCH → dead
|
|
70
|
+
*/
|
|
71
|
+
private isAlive(pid: number): boolean {
|
|
72
|
+
try {
|
|
73
|
+
process.kill(pid, 0);
|
|
74
|
+
return true;
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
77
|
+
if (code === 'EPERM') return true;
|
|
78
|
+
// ESRCH or any other error → treat as dead
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result aggregator — merges results from multiple parallel subagent
|
|
3
|
+
* executions into a single summary.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AggregatedResult, SubagentResult } from './types.js';
|
|
7
|
+
|
|
8
|
+
export function aggregate(results: SubagentResult[]): AggregatedResult {
|
|
9
|
+
if (results.length === 0) {
|
|
10
|
+
return {
|
|
11
|
+
status: 'all-passed',
|
|
12
|
+
totalTasks: 0,
|
|
13
|
+
completed: 0,
|
|
14
|
+
failed: 0,
|
|
15
|
+
totalUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
16
|
+
filesChanged: [],
|
|
17
|
+
combinedSummary: '',
|
|
18
|
+
durationMs: 0,
|
|
19
|
+
results: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const completed = results.filter((r) => r.exitCode === 0).length;
|
|
24
|
+
const failed = results.length - completed;
|
|
25
|
+
|
|
26
|
+
const status: AggregatedResult['status'] =
|
|
27
|
+
failed === 0 ? 'all-passed' : completed === 0 ? 'all-failed' : 'partial';
|
|
28
|
+
|
|
29
|
+
const totalUsage = {
|
|
30
|
+
inputTokens: 0,
|
|
31
|
+
outputTokens: 0,
|
|
32
|
+
totalTokens: 0,
|
|
33
|
+
};
|
|
34
|
+
for (const r of results) {
|
|
35
|
+
totalUsage.inputTokens += r.usage?.inputTokens ?? 0;
|
|
36
|
+
totalUsage.outputTokens += r.usage?.outputTokens ?? 0;
|
|
37
|
+
totalUsage.totalTokens += r.usage?.totalTokens ?? 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const fileSet = new Set<string>();
|
|
41
|
+
for (const r of results) {
|
|
42
|
+
if (r.filesChanged) {
|
|
43
|
+
for (const f of r.filesChanged) fileSet.add(f);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const combinedSummary = results
|
|
48
|
+
.filter((r) => r.summary)
|
|
49
|
+
.map((r) => `[${r.taskId}] ${r.summary}`)
|
|
50
|
+
.join('\n');
|
|
51
|
+
|
|
52
|
+
// Parallel wall-clock: max of all durations
|
|
53
|
+
const durationMs = Math.max(...results.map((r) => r.durationMs));
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
status,
|
|
57
|
+
totalTasks: results.length,
|
|
58
|
+
completed,
|
|
59
|
+
failed,
|
|
60
|
+
totalUsage,
|
|
61
|
+
filesChanged: [...fileSet],
|
|
62
|
+
combinedSummary,
|
|
63
|
+
durationMs,
|
|
64
|
+
results,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic task claim system — prevents two subagents from working the same task.
|
|
3
|
+
*
|
|
4
|
+
* Pure in-memory Map backing store. No async, no external deps.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ClaimInfo } from './types.js';
|
|
8
|
+
|
|
9
|
+
export class TaskCheckout {
|
|
10
|
+
private readonly claims = new Map<string, ClaimInfo>();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Attempt to claim a task for a claimer.
|
|
14
|
+
* Returns true if the claim succeeds (or the same claimer already holds it).
|
|
15
|
+
* Returns false if the task is already claimed by a different claimer.
|
|
16
|
+
*/
|
|
17
|
+
claim(taskId: string, claimerId: string): boolean {
|
|
18
|
+
const existing = this.claims.get(taskId);
|
|
19
|
+
if (existing) {
|
|
20
|
+
return existing.claimerId === claimerId;
|
|
21
|
+
}
|
|
22
|
+
this.claims.set(taskId, { taskId, claimerId, claimedAt: Date.now() });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Release a claimed task. Returns true if released, false if not claimed.
|
|
28
|
+
*/
|
|
29
|
+
release(taskId: string): boolean {
|
|
30
|
+
return this.claims.delete(taskId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get claim info for a task, or undefined if unclaimed.
|
|
35
|
+
*/
|
|
36
|
+
getClaimer(taskId: string): ClaimInfo | undefined {
|
|
37
|
+
return this.claims.get(taskId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List all active claims.
|
|
42
|
+
*/
|
|
43
|
+
listClaimed(): ClaimInfo[] {
|
|
44
|
+
return [...this.claims.values()];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check whether a task is available (unclaimed).
|
|
49
|
+
*/
|
|
50
|
+
isAvailable(taskId: string): boolean {
|
|
51
|
+
return !this.claims.has(taskId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clear all claims. Useful for cleanup between dispatch rounds.
|
|
56
|
+
*/
|
|
57
|
+
releaseAll(): void {
|
|
58
|
+
this.claims.clear();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent runtime engine — types for spawning, managing, and aggregating
|
|
3
|
+
* results from child agent processes.
|
|
4
|
+
*
|
|
5
|
+
* The SubagentDispatcher composes: TaskCheckout, WorkspaceResolver,
|
|
6
|
+
* ConcurrencyManager, OrphanReaper, and RuntimeAdapterRegistry.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AdapterSessionState, AdapterTokenUsage } from '../adapters/types.js';
|
|
10
|
+
|
|
11
|
+
// ─── Task ───────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/** A task to be dispatched to a subagent */
|
|
14
|
+
export interface SubagentTask {
|
|
15
|
+
/** Unique task identifier */
|
|
16
|
+
taskId: string;
|
|
17
|
+
/** The prompt or task description for the subagent */
|
|
18
|
+
prompt: string;
|
|
19
|
+
/** Working directory for execution */
|
|
20
|
+
workspace: string;
|
|
21
|
+
/** Runtime adapter type (e.g., 'claude-code', 'codex'). Falls back to registry default. */
|
|
22
|
+
runtime?: string;
|
|
23
|
+
/** Task IDs this task depends on (must complete first) */
|
|
24
|
+
dependencies?: string[];
|
|
25
|
+
/** Timeout in milliseconds. Default: 300_000 (5 min) */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/** Additional context to pass to the adapter */
|
|
28
|
+
config?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Status ─────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Lifecycle status of a subagent task */
|
|
34
|
+
export type SubagentStatus = 'queued' | 'claimed' | 'running' | 'completed' | 'failed' | 'orphaned';
|
|
35
|
+
|
|
36
|
+
// ─── Result ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/** Result from a single subagent execution */
|
|
39
|
+
export interface SubagentResult {
|
|
40
|
+
/** Task ID this result belongs to */
|
|
41
|
+
taskId: string;
|
|
42
|
+
/** Final status */
|
|
43
|
+
status: SubagentStatus;
|
|
44
|
+
/** Exit code from the adapter (0 = success) */
|
|
45
|
+
exitCode: number;
|
|
46
|
+
/** Human-readable summary of what the subagent did */
|
|
47
|
+
summary?: string;
|
|
48
|
+
/** Token usage */
|
|
49
|
+
usage?: AdapterTokenUsage;
|
|
50
|
+
/** Session state for potential resume */
|
|
51
|
+
sessionState?: AdapterSessionState;
|
|
52
|
+
/** Files changed by this subagent */
|
|
53
|
+
filesChanged?: string[];
|
|
54
|
+
/** Error message if failed */
|
|
55
|
+
error?: string;
|
|
56
|
+
/** Duration in milliseconds */
|
|
57
|
+
durationMs: number;
|
|
58
|
+
/** PID of the child process (if spawned) */
|
|
59
|
+
pid?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Dispatch Options ───────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/** Options controlling how tasks are dispatched */
|
|
65
|
+
export interface DispatchOptions {
|
|
66
|
+
/** Run tasks in parallel (default: true) */
|
|
67
|
+
parallel?: boolean;
|
|
68
|
+
/** Max concurrent subagents (default: 3) */
|
|
69
|
+
maxConcurrent?: number;
|
|
70
|
+
/** Isolate each task in a git worktree (default: false) */
|
|
71
|
+
worktreeIsolation?: boolean;
|
|
72
|
+
/** Global timeout per task in ms (default: 300_000) */
|
|
73
|
+
timeout?: number;
|
|
74
|
+
/** Callback for per-task status updates */
|
|
75
|
+
onTaskUpdate?: (taskId: string, status: SubagentStatus) => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Aggregated Result ──────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** Aggregated result from multiple subagent executions */
|
|
81
|
+
export interface AggregatedResult {
|
|
82
|
+
/** Overall status */
|
|
83
|
+
status: 'all-passed' | 'partial' | 'all-failed';
|
|
84
|
+
/** Total tasks dispatched */
|
|
85
|
+
totalTasks: number;
|
|
86
|
+
/** Count of completed tasks */
|
|
87
|
+
completed: number;
|
|
88
|
+
/** Count of failed tasks */
|
|
89
|
+
failed: number;
|
|
90
|
+
/** Sum of all token usage */
|
|
91
|
+
totalUsage: AdapterTokenUsage;
|
|
92
|
+
/** Deduplicated list of all files changed */
|
|
93
|
+
filesChanged: string[];
|
|
94
|
+
/** Combined summary from all tasks */
|
|
95
|
+
combinedSummary: string;
|
|
96
|
+
/** Total duration in milliseconds */
|
|
97
|
+
durationMs: number;
|
|
98
|
+
/** Per-task results */
|
|
99
|
+
results: SubagentResult[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Claim Info ─────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/** Information about a task claim */
|
|
105
|
+
export interface ClaimInfo {
|
|
106
|
+
/** Task ID */
|
|
107
|
+
taskId: string;
|
|
108
|
+
/** Agent/process that claimed this task */
|
|
109
|
+
claimerId: string;
|
|
110
|
+
/** When the claim was made */
|
|
111
|
+
claimedAt: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Worktree Info ──────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/** Information about an active git worktree */
|
|
117
|
+
export interface WorktreeInfo {
|
|
118
|
+
/** Task ID this worktree is for */
|
|
119
|
+
taskId: string;
|
|
120
|
+
/** Absolute path to the worktree */
|
|
121
|
+
path: string;
|
|
122
|
+
/** Branch name (if created) */
|
|
123
|
+
branch?: string;
|
|
124
|
+
/** When the worktree was created */
|
|
125
|
+
createdAt: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Tracked Process ────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/** A tracked child process for orphan detection */
|
|
131
|
+
export interface TrackedProcess {
|
|
132
|
+
/** Process ID */
|
|
133
|
+
pid: number;
|
|
134
|
+
/** Task ID this process is executing */
|
|
135
|
+
taskId: string;
|
|
136
|
+
/** When the process was registered */
|
|
137
|
+
registeredAt: number;
|
|
138
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceResolver — Git worktree isolation for subagent tasks.
|
|
3
|
+
*
|
|
4
|
+
* When isolation is requested, creates a dedicated git worktree per task
|
|
5
|
+
* at `<baseDir>/.soleri/worktrees/<taskId>/`. Falls back gracefully to the
|
|
6
|
+
* original workspace if git worktree creation fails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import type { WorktreeInfo } from './types.js';
|
|
14
|
+
|
|
15
|
+
const EXEC_OPTS = { encoding: 'utf-8' as const, timeout: 30_000 };
|
|
16
|
+
|
|
17
|
+
export class WorkspaceResolver {
|
|
18
|
+
private readonly baseDir: string;
|
|
19
|
+
private readonly worktrees = new Map<string, WorktreeInfo>();
|
|
20
|
+
|
|
21
|
+
constructor(baseDir: string) {
|
|
22
|
+
this.baseDir = baseDir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a workspace path for a task.
|
|
27
|
+
*
|
|
28
|
+
* If `isolate` is true, creates a git worktree at
|
|
29
|
+
* `<baseDir>/.soleri/worktrees/<taskId>/` on branch `subagent/<taskId>`.
|
|
30
|
+
* Returns the worktree path on success, or the original `workspace` on failure.
|
|
31
|
+
*
|
|
32
|
+
* If `isolate` is false, returns `workspace` as-is.
|
|
33
|
+
*/
|
|
34
|
+
resolve(taskId: string, workspace: string, isolate: boolean): string {
|
|
35
|
+
if (!isolate) {
|
|
36
|
+
return workspace;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const worktreePath = join(this.baseDir, '.soleri', 'worktrees', taskId);
|
|
41
|
+
const branch = `subagent/${taskId}`;
|
|
42
|
+
|
|
43
|
+
// Ensure parent directory exists
|
|
44
|
+
const parentDir = join(this.baseDir, '.soleri', 'worktrees');
|
|
45
|
+
if (!existsSync(parentDir)) {
|
|
46
|
+
mkdirSync(parentDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
execSync(`git worktree add "${worktreePath}" -b "${branch}"`, {
|
|
50
|
+
...EXEC_OPTS,
|
|
51
|
+
cwd: this.baseDir,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const info: WorktreeInfo = {
|
|
55
|
+
taskId,
|
|
56
|
+
path: worktreePath,
|
|
57
|
+
branch,
|
|
58
|
+
createdAt: Date.now(),
|
|
59
|
+
};
|
|
60
|
+
this.worktrees.set(taskId, info);
|
|
61
|
+
|
|
62
|
+
return worktreePath;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// Graceful fallback — log warning and return original workspace
|
|
65
|
+
console.warn(
|
|
66
|
+
`[WorkspaceResolver] Failed to create worktree for task "${taskId}":`,
|
|
67
|
+
err instanceof Error ? err.message : err,
|
|
68
|
+
);
|
|
69
|
+
return workspace;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Remove the worktree for a given task.
|
|
75
|
+
* Silently handles errors (e.g., worktree already removed).
|
|
76
|
+
*/
|
|
77
|
+
cleanup(taskId: string): void {
|
|
78
|
+
const info = this.worktrees.get(taskId);
|
|
79
|
+
if (!info) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
execSync(`git worktree remove "${info.path}" --force`, { ...EXEC_OPTS, cwd: this.baseDir });
|
|
85
|
+
} catch {
|
|
86
|
+
// Silently ignore — worktree may already be gone
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Clean up the branch as well
|
|
90
|
+
if (info.branch) {
|
|
91
|
+
try {
|
|
92
|
+
execSync(`git branch -D "${info.branch}"`, { ...EXEC_OPTS, cwd: this.baseDir });
|
|
93
|
+
} catch {
|
|
94
|
+
// Silently ignore — branch may not exist
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.worktrees.delete(taskId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Remove all active worktrees. */
|
|
102
|
+
cleanupAll(): void {
|
|
103
|
+
for (const taskId of Array.from(this.worktrees.keys())) {
|
|
104
|
+
this.cleanup(taskId);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Return all currently active worktrees. */
|
|
109
|
+
listActive(): WorktreeInfo[] {
|
|
110
|
+
return [...this.worktrees.values()];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Check whether a worktree exists for the given task. */
|
|
114
|
+
isActive(taskId: string): boolean {
|
|
115
|
+
return this.worktrees.has(taskId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -10,6 +10,7 @@ import { Vault } from './vault.js';
|
|
|
10
10
|
import { Brain } from '../brain/brain.js';
|
|
11
11
|
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
12
12
|
|
|
13
|
+
const isCI = !!process.env.CI;
|
|
13
14
|
const DOMAINS = ['design', 'a11y', 'performance', 'security', 'architecture', 'testing', 'ux'];
|
|
14
15
|
const TYPES: IntelligenceEntry['type'][] = ['pattern', 'anti-pattern', 'rule', 'playbook'];
|
|
15
16
|
const SEVERITIES: IntelligenceEntry['severity'][] = ['critical', 'warning', 'suggestion'];
|
|
@@ -76,7 +77,7 @@ describe('Vault Scaling — 10K entries', () => {
|
|
|
76
77
|
const elapsed = performance.now() - start;
|
|
77
78
|
|
|
78
79
|
expect(results.length).toBeGreaterThan(0);
|
|
79
|
-
expect(elapsed).toBeLessThan(50);
|
|
80
|
+
expect(elapsed).toBeLessThan(isCI ? 500 : 50);
|
|
80
81
|
}
|
|
81
82
|
});
|
|
82
83
|
|
|
@@ -89,7 +90,7 @@ describe('Vault Scaling — 10K entries', () => {
|
|
|
89
90
|
const elapsed = performance.now() - start;
|
|
90
91
|
|
|
91
92
|
expect(results.length).toBeGreaterThan(0);
|
|
92
|
-
expect(elapsed).toBeLessThan(50);
|
|
93
|
+
expect(elapsed).toBeLessThan(isCI ? 500 : 50);
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
test('list with filters under 200ms at 10K', () => {
|
package/vitest.config.ts
CHANGED