@astrale-os/cli 0.7.0-alpha.0 → 0.8.1-alpha.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.
Files changed (111) hide show
  1. package/README.md +7 -3
  2. package/dist/astrale.js +128 -98
  3. package/package.json +11 -5
  4. package/src/commands/studio.ts +13 -0
  5. package/src/lib/__tests__/view-assets.test.ts +114 -0
  6. package/src/lib/instance.ts +0 -11
  7. package/src/lib/login-flow.ts +4 -41
  8. package/src/lib/view/assets.ts +64 -0
  9. package/src/lib/view/server.ts +4 -37
  10. package/studio/client/dist/assets/elk-api-DVw5WDTH.js +1 -0
  11. package/studio/client/dist/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  12. package/studio/client/dist/assets/index-BQnJ5sgd.css +1 -0
  13. package/studio/client/dist/assets/index-CTbkDp3z.js +81 -0
  14. package/studio/client/dist/assets/index-ChOr3yP0.css +1 -0
  15. package/studio/client/dist/assets/index-Dl709Ra1.js +8 -0
  16. package/studio/client/dist/index.html +2 -2
  17. package/studio/server/agent/ask.test.ts +74 -0
  18. package/studio/server/agent/ask.ts +20 -22
  19. package/studio/server/agent/bridge/client.test.ts +49 -0
  20. package/studio/server/agent/bridge/client.ts +37 -0
  21. package/studio/server/agent/bridge/grant.test.ts +62 -0
  22. package/studio/server/agent/bridge/grant.ts +84 -0
  23. package/studio/server/agent/bridge/routes.test.ts +151 -0
  24. package/studio/server/agent/bridge/routes.ts +148 -0
  25. package/studio/server/agent/{bridge-mcp.ts → bridge/stdio.ts} +27 -21
  26. package/studio/server/agent/conversation.test.ts +84 -0
  27. package/studio/server/agent/conversation.ts +110 -0
  28. package/studio/server/agent/{types.ts → harness/adapter.ts} +63 -12
  29. package/studio/server/agent/harness/claude/adapter.test.ts +338 -0
  30. package/studio/server/agent/harness/claude/adapter.ts +71 -0
  31. package/studio/server/agent/harness/claude/ask.ts +114 -0
  32. package/studio/server/agent/harness/claude/capabilities.ts +27 -0
  33. package/studio/server/agent/harness/claude/command.ts +63 -0
  34. package/studio/server/agent/harness/claude/events.ts +215 -0
  35. package/studio/server/agent/harness/claude/loadout.ts +132 -0
  36. package/studio/server/agent/harness/claude/mcp.test.ts +36 -0
  37. package/studio/server/agent/harness/claude/mcp.ts +41 -0
  38. package/studio/server/agent/harness/claude/skills.ts +43 -0
  39. package/studio/server/agent/harness/codex/adapter.test.ts +269 -0
  40. package/studio/server/agent/harness/codex/adapter.ts +116 -0
  41. package/studio/server/agent/harness/codex/ask.test.ts +176 -0
  42. package/studio/server/agent/harness/codex/ask.ts +181 -0
  43. package/studio/server/agent/harness/codex/command.test.ts +69 -0
  44. package/studio/server/agent/harness/codex/command.ts +43 -0
  45. package/studio/server/agent/harness/codex/events.test.ts +94 -0
  46. package/studio/server/agent/harness/codex/events.ts +132 -0
  47. package/studio/server/agent/harness/codex/exec.ts +113 -0
  48. package/studio/server/agent/harness/codex/loadout.ts +63 -0
  49. package/studio/server/agent/harness/codex/mcp.test.ts +32 -0
  50. package/studio/server/agent/harness/codex/mcp.ts +27 -0
  51. package/studio/server/agent/harness/codex/models.test.ts +167 -0
  52. package/studio/server/agent/harness/codex/models.ts +211 -0
  53. package/studio/server/agent/harness/codex/skills.ts +33 -0
  54. package/studio/server/agent/harness/gateway/config.test.ts +98 -0
  55. package/studio/server/{state/harness-gateway.ts → agent/harness/gateway/config.ts} +30 -8
  56. package/studio/server/agent/harness/gateway/token.test.ts +133 -0
  57. package/studio/server/agent/harness/gateway/token.ts +166 -0
  58. package/studio/server/agent/harness/mock/adapter.ts +190 -0
  59. package/studio/server/agent/harness/mock/domain-edit.ts +41 -0
  60. package/studio/server/agent/harness/process.test.ts +46 -0
  61. package/studio/server/agent/harness/process.ts +104 -0
  62. package/studio/server/agent/harness/registry.ts +37 -0
  63. package/studio/server/agent/harness/selection.test.ts +75 -0
  64. package/studio/server/agent/harness/selection.ts +77 -0
  65. package/studio/server/agent/harness/skills.test.ts +95 -0
  66. package/studio/server/agent/harness/skills.ts +100 -0
  67. package/studio/server/agent/layout.test.ts +95 -0
  68. package/studio/server/agent/notify.ts +12 -0
  69. package/studio/server/agent/{schema-map.ts → prompts/anchors.ts} +2 -2
  70. package/studio/server/agent/prompts/ask.ts +34 -0
  71. package/studio/server/agent/{prompt.ts → prompts/system.ts} +3 -107
  72. package/studio/server/agent/prompts/turn.ts +57 -0
  73. package/studio/server/agent/routes.test.ts +176 -0
  74. package/studio/server/agent/routes.ts +214 -0
  75. package/studio/server/agent/run/completion.ts +193 -0
  76. package/studio/server/agent/run/coordinator.test.ts +337 -0
  77. package/studio/server/agent/run/coordinator.ts +104 -0
  78. package/studio/server/agent/run/live-state.ts +73 -0
  79. package/studio/server/agent/run/preparation.ts +162 -0
  80. package/studio/server/agent/run/transcript.test.ts +47 -0
  81. package/studio/server/agent/run/transcript.ts +30 -0
  82. package/studio/server/agent/run/usage.test.ts +46 -0
  83. package/studio/server/{state → agent/run}/usage.ts +4 -4
  84. package/studio/server/agent/stream.ts +37 -0
  85. package/studio/server/agent/{session-id.ts → telemetry.ts} +1 -1
  86. package/studio/server/api-agent-loadout.test.ts +69 -0
  87. package/studio/server/api.ts +3 -155
  88. package/studio/server/index.ts +6 -5
  89. package/studio/server/state/comments.test.ts +29 -0
  90. package/studio/server/state/comments.ts +3 -3
  91. package/studio/server/state/settings.test.ts +78 -0
  92. package/studio/server/state/settings.ts +28 -1
  93. package/studio/shared/agent-effort.test.ts +12 -0
  94. package/studio/shared/agent-effort.ts +15 -0
  95. package/studio/shared/agent-models.test.ts +9 -0
  96. package/studio/shared/agent-models.ts +14 -0
  97. package/studio/shared/settings-values.test.ts +18 -0
  98. package/studio/shared/settings-values.ts +20 -0
  99. package/studio/shared/types.ts +58 -7
  100. package/studio/tsconfig.json +23 -0
  101. package/viewer/tsconfig.json +13 -0
  102. package/src/connect-core.test.ts +0 -42
  103. package/src/connect-core.ts +0 -53
  104. package/studio/client/dist/assets/index-DAC1a9vW.js +0 -109
  105. package/studio/client/dist/assets/index-huaFafBC.css +0 -1
  106. package/studio/server/agent/bridge.ts +0 -188
  107. package/studio/server/agent/claude.ts +0 -678
  108. package/studio/server/agent/mock.ts +0 -186
  109. package/studio/server/agent/registry.ts +0 -29
  110. package/studio/server/agent/runner.ts +0 -487
  111. package/studio/server/state/harness-token.ts +0 -0
@@ -0,0 +1,148 @@
1
+ import type { StudioEvent } from '../../../shared/types'
2
+ import type { DomainHandle } from '../../domain'
3
+
4
+ import { addThreadEntry, readComments, setStatus, upsertComment } from '../../state/comments'
5
+ import { emitStudioEvent } from '../notify'
6
+
7
+ interface RunBridge {
8
+ domainId: string
9
+ root: string
10
+ notify: (event: StudioEvent) => void
11
+ onReply: (commentId: string, text: string) => void
12
+ onProgress: (text: string) => void
13
+ }
14
+
15
+ export interface BridgeSession {
16
+ token: string
17
+ onReply(callback: (commentId: string, text: string) => void): void
18
+ onProgress(callback: (text: string) => void): void
19
+ invoke(sub: string, body: Record<string, unknown>): Promise<Response>
20
+ dispose(): void
21
+ }
22
+
23
+ const bridges = new Map<string, RunBridge>()
24
+
25
+ function ok(data: unknown): Response {
26
+ return new Response(JSON.stringify(data), {
27
+ status: 200,
28
+ headers: { 'content-type': 'application/json' },
29
+ })
30
+ }
31
+
32
+ function error(message: string, status = 400): Response {
33
+ return new Response(JSON.stringify({ error: message }), {
34
+ status,
35
+ headers: { 'content-type': 'application/json' },
36
+ })
37
+ }
38
+
39
+ /** Open the authenticated write-back session owned by one agent run. */
40
+ export function openBridgeSession(
41
+ handle: DomainHandle,
42
+ token: string,
43
+ notify: (event: StudioEvent) => void,
44
+ ): BridgeSession {
45
+ const bridge: RunBridge = {
46
+ domainId: handle.id,
47
+ root: handle.root,
48
+ notify,
49
+ onReply: () => {},
50
+ onProgress: () => {},
51
+ }
52
+ bridges.set(token, bridge)
53
+ return {
54
+ token,
55
+ onReply: (callback) => {
56
+ bridge.onReply = callback
57
+ },
58
+ onProgress: (callback) => {
59
+ bridge.onProgress = callback
60
+ },
61
+ invoke: (sub, body) => applyBridgeCall(handle, sub, { ...body, token }),
62
+ dispose: () => {
63
+ bridges.delete(token)
64
+ },
65
+ }
66
+ }
67
+
68
+ /** Handle one token-guarded bridge route. */
69
+ export function handleBridge(
70
+ handle: DomainHandle,
71
+ sub: string,
72
+ _req: Request,
73
+ body: any,
74
+ ): Promise<Response> {
75
+ return applyBridgeCall(handle, sub, body)
76
+ }
77
+
78
+ async function applyBridgeCall(handle: DomainHandle, sub: string, body: any): Promise<Response> {
79
+ const token = String(body?.token ?? '')
80
+ const bridge = bridges.get(token)
81
+ if (!bridge || bridge.domainId !== handle.id) return error('invalid or expired bridge token', 401)
82
+ const root = bridge.root
83
+
84
+ switch (sub) {
85
+ case 'threads': {
86
+ const open = readComments(root).comments.filter(
87
+ (comment) => comment.status === 'open' && comment.thread.at(-1)?.role !== 'author',
88
+ )
89
+ return ok({
90
+ threads: open.map((comment) => ({
91
+ id: comment.id,
92
+ kind: comment.kind,
93
+ anchor: comment.anchorRefs?.[0]?.ref ?? null,
94
+ file: comment.anchorRefs?.[0]?.file ?? null,
95
+ latest: comment.thread.at(-1)?.text ?? '',
96
+ })),
97
+ })
98
+ }
99
+ case 'reply': {
100
+ const commentId = String(body.commentId ?? '')
101
+ const text = String(body.text ?? '').trim()
102
+ if (!commentId || !text) return error('commentId and text are required')
103
+ const comment = addThreadEntry(root, commentId, {
104
+ role: 'author',
105
+ type: body.options ? 'choice' : 'text',
106
+ text,
107
+ options: body.options,
108
+ answer: body.answer,
109
+ })
110
+ if (!comment) return error('unknown commentId', 404)
111
+ if (body.resolve) setStatus(root, commentId, 'closed', body.closeNote)
112
+ bridge.onReply(commentId, text)
113
+ emitStudioEvent(bridge.notify, { type: 'comments', domainId: handle.id })
114
+ return ok({ ok: true, resolved: !!body.resolve })
115
+ }
116
+ case 'resolve': {
117
+ const commentId = String(body.commentId ?? '')
118
+ if (!commentId) return error('commentId is required')
119
+ const comment = setStatus(root, commentId, 'closed', body.closeNote)
120
+ if (!comment) return error('unknown commentId', 404)
121
+ bridge.onReply(commentId, body.closeNote ? `resolved: ${body.closeNote}` : 'resolved')
122
+ emitStudioEvent(bridge.notify, { type: 'comments', domainId: handle.id })
123
+ return ok({ ok: true })
124
+ }
125
+ case 'progress': {
126
+ const text = String(body.text ?? '').trim()
127
+ if (text) bridge.onProgress(text)
128
+ return ok({ ok: true })
129
+ }
130
+ case 'raise_question': {
131
+ const ref = String(body.ref ?? '')
132
+ const text = String(body.text ?? '').trim()
133
+ if (!ref || !text) return error('ref and text are required')
134
+ const comment = upsertComment(root, {
135
+ anchors: [body.label ?? ref],
136
+ anchorRefs: [{ ref, kind: (body.kind ?? 'schema') as any, file: body.file }],
137
+ text,
138
+ firstRole: 'author',
139
+ type: body.options ? 'choice' : 'text',
140
+ options: body.options,
141
+ })
142
+ emitStudioEvent(bridge.notify, { type: 'comments', domainId: handle.id })
143
+ return ok({ ok: true, id: comment.id })
144
+ }
145
+ default:
146
+ return error('unknown bridge route', 404)
147
+ }
148
+ }
@@ -1,15 +1,31 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * bridge-mcp.ts — a minimal stdio MCP server the harness spawns (via the
4
- * generated --mcp-config). It exposes the Domain Studio write-back tools and
3
+ * bridge/stdio.ts — the stdio MCP server spawned by local harnesses. It exposes
4
+ * the Domain Studio write-back tools from the generated MCP configuration and
5
5
  * forwards each call to the token-guarded HTTP bridge (api → agent/bridge/*),
6
6
  * so an agent can reply to comment threads and post progress AS IT WORKS.
7
7
  *
8
8
  * Transport: newline-delimited JSON-RPC 2.0 on stdin/stdout. Nothing but
9
9
  * protocol messages may touch stdout; diagnostics go to stderr.
10
10
  */
11
- const BASE = process.env.DOMAIN_STUDIO_BRIDGE_URL || ''
12
- const TOKEN = process.env.DOMAIN_STUDIO_BRIDGE_TOKEN || ''
11
+ import { readFileSync } from 'node:fs'
12
+
13
+ import { forwardBridgeTool } from './client'
14
+
15
+ function configFromArgv(): { base?: string; token?: string } {
16
+ const i = process.argv.indexOf('--config')
17
+ const path = i >= 0 ? process.argv[i + 1] : undefined
18
+ if (!path) return {}
19
+ try {
20
+ return JSON.parse(readFileSync(path, 'utf8')) as { base?: string; token?: string }
21
+ } catch {
22
+ return {}
23
+ }
24
+ }
25
+
26
+ const config = configFromArgv()
27
+ const BASE = config.base || process.env.DOMAIN_STUDIO_BRIDGE_URL || ''
28
+ const TOKEN = config.token || process.env.DOMAIN_STUDIO_BRIDGE_TOKEN || ''
13
29
 
14
30
  const TOOLS = [
15
31
  {
@@ -106,21 +122,6 @@ function replyError(id: unknown, code: number, message: string): void {
106
122
  send({ jsonrpc: '2.0', id, error: { code, message } })
107
123
  }
108
124
 
109
- async function callBridge(route: string, args: Record<string, unknown>): Promise<string> {
110
- if (!BASE || !TOKEN) return 'bridge not configured'
111
- try {
112
- const res = await fetch(`${BASE}/${route}`, {
113
- method: 'POST',
114
- headers: { 'content-type': 'application/json' },
115
- body: JSON.stringify({ token: TOKEN, ...args }),
116
- })
117
- const text = await res.text()
118
- return res.ok ? text || 'ok' : `error ${res.status}: ${text}`
119
- } catch (e: any) {
120
- return `bridge call failed: ${e?.message ?? e}`
121
- }
122
- }
123
-
124
125
  async function handle(req: any): Promise<void> {
125
126
  const { id, method, params } = req
126
127
  switch (method) {
@@ -152,8 +153,13 @@ async function handle(req: any): Promise<void> {
152
153
  replyError(id, -32602, `unknown tool: ${params?.name}`)
153
154
  return
154
155
  }
155
- const out = await callBridge(tool.route, (params?.arguments ?? {}) as Record<string, unknown>)
156
- reply(id, { content: [{ type: 'text', text: out }] })
156
+ const result = await forwardBridgeTool(
157
+ BASE,
158
+ TOKEN,
159
+ tool.route,
160
+ (params?.arguments ?? {}) as Record<string, unknown>,
161
+ )
162
+ reply(id, result)
157
163
  return
158
164
  }
159
165
  default:
@@ -0,0 +1,84 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { mkdtempSync, rmSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+
6
+ import { readJson, writeJson } from '../state/store'
7
+ import {
8
+ clearConversation,
9
+ readConversation,
10
+ saveConversation,
11
+ setConversationSession,
12
+ } from './conversation'
13
+
14
+ const roots: string[] = []
15
+ const root = () => {
16
+ const value = mkdtempSync(join(tmpdir(), 'studio-conversation-'))
17
+ roots.push(value)
18
+ return value
19
+ }
20
+
21
+ afterEach(() => {
22
+ while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
23
+ })
24
+
25
+ describe('per-harness conversations', () => {
26
+ test('preserves independent Claude and Codex sessions', () => {
27
+ const dir = root()
28
+ saveConversation(dir, 'claude', {
29
+ sessionId: 'claude-session',
30
+ turns: 2,
31
+ updatedAt: 'now',
32
+ })
33
+ saveConversation(dir, 'codex', {
34
+ sessionId: 'codex-thread',
35
+ turns: 3,
36
+ updatedAt: 'later',
37
+ })
38
+
39
+ expect(readConversation(dir, 'claude')).toMatchObject({
40
+ sessionId: 'claude-session',
41
+ turns: 2,
42
+ })
43
+ expect(readConversation(dir, 'codex')).toMatchObject({
44
+ sessionId: 'codex-thread',
45
+ turns: 3,
46
+ })
47
+ clearConversation(dir, 'codex')
48
+ expect(readConversation(dir, 'claude').sessionId).toBe('claude-session')
49
+ expect(readConversation(dir, 'codex').sessionId).toBeUndefined()
50
+ })
51
+
52
+ test('migrates the legacy single-session shape on first write', () => {
53
+ const dir = root()
54
+ writeJson(dir, '.cache/agent/session.json', {
55
+ harness: 'claude',
56
+ sessionId: 'legacy-session',
57
+ turns: 4,
58
+ updatedAt: 'then',
59
+ })
60
+ expect(readConversation(dir, 'claude').turns).toBe(4)
61
+
62
+ setConversationSession(dir, 'codex', 'codex-thread')
63
+ const stored = readJson<any>(dir, '.cache/agent/session.json', {})
64
+ expect(stored.version).toBe(1)
65
+ expect(stored.conversations.claude.sessionId).toBe('legacy-session')
66
+ expect(stored.conversations.codex.sessionId).toBe('codex-thread')
67
+ })
68
+
69
+ test('fails closed without rewriting a future conversation-store version', () => {
70
+ const dir = root()
71
+ const future = {
72
+ version: 2,
73
+ conversations: {
74
+ codex: { sessionId: 'future-thread', turns: 9 },
75
+ },
76
+ }
77
+ writeJson(dir, '.cache/agent/session.json', future)
78
+
79
+ expect(() => setConversationSession(dir, 'claude', 'new-session')).toThrow(
80
+ 'unsupported agent conversation store version: 2',
81
+ )
82
+ expect(readJson<any>(dir, '.cache/agent/session.json', {})).toEqual(future)
83
+ })
84
+ })
@@ -0,0 +1,110 @@
1
+ import type { AgentSessionInfo, ConversationInfo } from '../../shared/types'
2
+
3
+ import { readJson, removeState, writeJson } from '../state/store'
4
+
5
+ const SESSION_FILE = '.cache/agent/session.json'
6
+
7
+ interface HarnessConversation {
8
+ sessionId?: string
9
+ turns?: number
10
+ updatedAt?: string
11
+ }
12
+
13
+ interface ConversationStore {
14
+ version: 1
15
+ conversations: Record<string, HarnessConversation>
16
+ }
17
+
18
+ interface LegacySessionState extends HarnessConversation {
19
+ harness?: string
20
+ }
21
+
22
+ function readStore(root: string): ConversationStore {
23
+ const raw = readJson<ConversationStore | LegacySessionState | null>(root, SESSION_FILE, null)
24
+ if (!raw) return { version: 1, conversations: {} }
25
+ if ('conversations' in raw) {
26
+ if (raw.version !== 1)
27
+ throw new Error(`unsupported agent conversation store version: ${String(raw.version)}`)
28
+ if (raw.conversations) return { version: 1, conversations: { ...raw.conversations } }
29
+ }
30
+ if ('harness' in raw && raw.harness && raw.sessionId) {
31
+ return {
32
+ version: 1,
33
+ conversations: {
34
+ [raw.harness]: {
35
+ sessionId: raw.sessionId,
36
+ turns: raw.turns ?? 0,
37
+ updatedAt: raw.updatedAt,
38
+ },
39
+ },
40
+ }
41
+ }
42
+ return { version: 1, conversations: {} }
43
+ }
44
+
45
+ function writeStore(root: string, store: ConversationStore): void {
46
+ const entries = Object.entries(store.conversations).filter(([, c]) => !!c.sessionId)
47
+ if (entries.length === 0) {
48
+ removeState(root, SESSION_FILE)
49
+ return
50
+ }
51
+ writeJson(root, SESSION_FILE, {
52
+ version: 1,
53
+ conversations: Object.fromEntries(entries),
54
+ } satisfies ConversationStore)
55
+ }
56
+
57
+ export function readConversation(root: string, harness: string): HarnessConversation {
58
+ return readStore(root).conversations[harness] ?? {}
59
+ }
60
+
61
+ export function conversationInfo(root: string, harness: string): ConversationInfo {
62
+ const conversation = readConversation(root, harness)
63
+ return {
64
+ active: !!conversation.sessionId,
65
+ turns: conversation.turns ?? 0,
66
+ harness: conversation.sessionId ? harness : undefined,
67
+ }
68
+ }
69
+
70
+ export function sessionInfo(root: string, harness: string): AgentSessionInfo {
71
+ const conversation = readConversation(root, harness)
72
+ return {
73
+ sessionId: conversation.sessionId ?? null,
74
+ turns: conversation.turns ?? 0,
75
+ harness: conversation.sessionId ? harness : undefined,
76
+ }
77
+ }
78
+
79
+ export function saveConversation(
80
+ root: string,
81
+ harness: string,
82
+ conversation: Required<Pick<HarnessConversation, 'sessionId' | 'turns'>> &
83
+ Pick<HarnessConversation, 'updatedAt'>,
84
+ ): void {
85
+ const store = readStore(root)
86
+ store.conversations[harness] = conversation
87
+ writeStore(root, store)
88
+ }
89
+
90
+ export function setConversationSession(root: string, harness: string, sessionId: string): void {
91
+ const trimmed = sessionId.trim()
92
+ if (!trimmed) {
93
+ clearConversation(root, harness)
94
+ return
95
+ }
96
+ const store = readStore(root)
97
+ const previous = store.conversations[harness] ?? {}
98
+ store.conversations[harness] = {
99
+ sessionId: trimmed,
100
+ turns: previous.turns ?? 0,
101
+ updatedAt: new Date().toISOString(),
102
+ }
103
+ writeStore(root, store)
104
+ }
105
+
106
+ export function clearConversation(root: string, harness: string): void {
107
+ const store = readStore(root)
108
+ delete store.conversations[harness]
109
+ writeStore(root, store)
110
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * agent/types.ts — the harness-agnostic contract for "run one agent turn".
2
+ * harness/adapter.ts — the executable boundary implemented by each local agent
3
+ * harness.
3
4
  *
4
5
  * A *harness* is whatever local AI agent does the edits (Claude Code, Codex, …).
5
6
  * The studio never talks to a model API directly; it shells out to the harness
@@ -7,7 +8,13 @@
7
8
  * harness maps its native streaming output onto `AgentStreamEvent`s and returns
8
9
  * a final text blob (which carries the machine-state reply block).
9
10
  */
10
- import type { AgentEffort, AgentEventKind, HarnessLoadout } from '../../shared/types'
11
+ import type {
12
+ AgentAccess,
13
+ AgentEffort,
14
+ AgentEventKind,
15
+ HarnessCapabilities,
16
+ HarnessLoadout,
17
+ } from '../../../shared/types'
11
18
 
12
19
  /** A single normalized activity event emitted while the harness runs. */
13
20
  export interface AgentStreamEvent {
@@ -17,6 +24,21 @@ export interface AgentStreamEvent {
17
24
  target?: string
18
25
  }
19
26
 
27
+ /** Harness-neutral description of one stdio MCP server. Each harness is
28
+ * responsible for expressing this in its native CLI/config shape. */
29
+ export interface HarnessMcpServer {
30
+ name: string
31
+ command: string
32
+ args?: string[]
33
+ env?: Record<string, string>
34
+ required?: boolean
35
+ /** Studio's run-scoped write-back tools are user-authorized by the submit. */
36
+ approvalMode?: 'auto' | 'prompt' | 'writes' | 'approve'
37
+ enabledTools?: string[]
38
+ /** In-process harnesses may invoke the same grant without spawning stdio. */
39
+ invoke?: (tool: string, args: Record<string, unknown>) => Promise<unknown>
40
+ }
41
+
20
42
  export interface AgentTurnInput {
21
43
  /** the domain repo root — the agent's working directory */
22
44
  root: string
@@ -26,13 +48,16 @@ export interface AgentTurnInput {
26
48
  appendSystemPrompt?: string
27
49
  /** prior harness session id → resume the same conversation */
28
50
  sessionId?: string
51
+ /** explicit model override; absent preserves the harness's native config/default */
52
+ model?: string
29
53
  /** harness reasoning effort for this turn */
30
54
  effort?: AgentEffort
31
- /** path to a generated MCP config exposing the studio write-back bridge */
32
- mcpConfigPath?: string
33
- /** extra env merged into the harness CHILD process only (e.g. ANTHROPIC_BASE_URL
34
- * / ANTHROPIC_AUTH_TOKEN to route through a custom model gateway). Never touches
35
- * the studio's own env or the user's shell / global `claude`. */
55
+ /** authority granted to the local harness */
56
+ access?: AgentAccess
57
+ /** generated MCP servers exposing Studio write-back */
58
+ mcpServers?: HarnessMcpServer[]
59
+ /** extra env merged into the harness CHILD process only. Never touches the
60
+ * studio's own env or the user's shell/global harness process. */
36
61
  env?: Record<string, string>
37
62
  /** abort the turn (user pressed Cancel) */
38
63
  signal: AbortSignal
@@ -47,7 +72,7 @@ export interface AgentTurnResult {
47
72
  finalText: string
48
73
  costUsd?: number
49
74
  numTurns?: number
50
- /** total tokens processed (input + output + cache) */
75
+ /** total token usage reported by the harness */
51
76
  tokens?: number
52
77
  isError: boolean
53
78
  errorMessage?: string
@@ -68,9 +93,13 @@ export interface AskInput {
68
93
  appendSystemPrompt?: string
69
94
  /** the domain's live conversation session id to FORK from (undefined ⇒ fresh, no fork) */
70
95
  sessionId?: string
96
+ /** explicit model override; absent preserves the harness's native config/default */
97
+ model?: string
71
98
  /** harness reasoning effort for this side question */
72
99
  effort?: AgentEffort
73
- /** extra env merged into the harness child only (custom model gateway) — see AgentTurnInput.env */
100
+ /** authority granted to the local harness */
101
+ access?: AgentAccess
102
+ /** extra env merged into the harness child only — see AgentTurnInput.env */
74
103
  env?: Record<string, string>
75
104
  signal: AbortSignal
76
105
  /** called with each chunk of the answer as it streams in */
@@ -93,16 +122,26 @@ export interface HarnessHealth {
93
122
  detail?: string
94
123
  }
95
124
 
125
+ export interface HarnessLoadoutOptions {
126
+ /** Child-only environment overrides used while probing the harness. */
127
+ env?: Record<string, string>
128
+ /** Studio's per-domain override for the selected harness. */
129
+ model?: string
130
+ /** Bypass the adapter's short-lived probe cache for an explicit user re-probe. */
131
+ refresh?: boolean
132
+ }
133
+
96
134
  export interface AgentHarness {
97
135
  /** stable id, e.g. 'claude' | 'mock' | 'codex' */
98
136
  id: string
99
137
  /** human label for the UI */
100
138
  label: string
139
+ capabilities: HarnessCapabilities
101
140
  /** is the underlying CLI/binary actually invokable here? */
102
- isAvailable(): Promise<boolean>
141
+ isAvailable(signal?: AbortSignal): Promise<boolean>
103
142
  /** optional: a richer install probe for the UI (version + reason). Harnesses that
104
143
  * omit it fall back to isAvailable(). */
105
- health?(): Promise<HarnessHealth>
144
+ health?(signal?: AbortSignal): Promise<HarnessHealth>
106
145
  run(input: AgentTurnInput): Promise<AgentTurnResult>
107
146
  /** optional: answer a quick forked side question (Ask). Harnesses without a
108
147
  * fork concept simply omit this; the studio falls back gracefully. */
@@ -111,10 +150,22 @@ export interface AgentHarness {
111
150
  * tools, agents) — a read-only window into the agent for the Settings page.
112
151
  * Harnesses without an introspectable loadout omit it. `env` (e.g. a custom
113
152
  * model gateway) is merged into the probe child so the reported model reflects it. */
114
- loadout?(root: string, env?: Record<string, string>): Promise<HarnessLoadout>
153
+ loadout?(root: string, options?: HarnessLoadoutOptions): Promise<HarnessLoadout>
115
154
  /** optional: the raw SKILL.md for a skill command, so the UI can show it. */
116
155
  skillContent?(
117
156
  root: string,
118
157
  command: string,
119
158
  ): Promise<{ command: string; content: string; path: string } | null>
120
159
  }
160
+
161
+ /** Normalize optional rich health probes into one route-facing result. */
162
+ export async function inspectHarnessHealth(
163
+ harness: AgentHarness,
164
+ signal?: AbortSignal,
165
+ ): Promise<HarnessHealth> {
166
+ if (harness.health) return harness.health(signal)
167
+ return {
168
+ ok: await harness.isAvailable(signal),
169
+ bin: harness.id,
170
+ }
171
+ }