@astrale-os/cli 0.4.0-alpha.13

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 (219) hide show
  1. package/.check-workspace.cjs +40 -0
  2. package/README.md +151 -0
  3. package/dist/astrale.js +59749 -0
  4. package/package.json +90 -0
  5. package/src/command.ts +40 -0
  6. package/src/commands/__tests__/admin-instance.test.ts +73 -0
  7. package/src/commands/__tests__/auth-login.test.ts +178 -0
  8. package/src/commands/__tests__/auth-token.test.ts +223 -0
  9. package/src/commands/__tests__/call.test.ts +72 -0
  10. package/src/commands/__tests__/domain-list.test.ts +74 -0
  11. package/src/commands/__tests__/help-contract.test.ts +136 -0
  12. package/src/commands/__tests__/install-identity-override.test.ts +65 -0
  13. package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
  14. package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
  15. package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
  16. package/src/commands/__tests__/logs.test.ts +117 -0
  17. package/src/commands/__tests__/ls.test.ts +25 -0
  18. package/src/commands/__tests__/setup-plan.test.ts +61 -0
  19. package/src/commands/admin/status.ts +61 -0
  20. package/src/commands/admin/use.ts +77 -0
  21. package/src/commands/auth/login.ts +82 -0
  22. package/src/commands/auth/logout.ts +50 -0
  23. package/src/commands/auth/status.ts +86 -0
  24. package/src/commands/auth/token.ts +162 -0
  25. package/src/commands/browser.ts +207 -0
  26. package/src/commands/call.ts +300 -0
  27. package/src/commands/describe.ts +182 -0
  28. package/src/commands/domain/install.ts +420 -0
  29. package/src/commands/domain/list.ts +154 -0
  30. package/src/commands/domain/publish.ts +155 -0
  31. package/src/commands/get.ts +60 -0
  32. package/src/commands/identity/create.ts +26 -0
  33. package/src/commands/identity/delete.ts +18 -0
  34. package/src/commands/identity/export.ts +66 -0
  35. package/src/commands/identity/import.ts +101 -0
  36. package/src/commands/identity/list.ts +56 -0
  37. package/src/commands/identity/register.ts +170 -0
  38. package/src/commands/identity/sync.ts +32 -0
  39. package/src/commands/identity/unsync.ts +24 -0
  40. package/src/commands/identity/use.ts +18 -0
  41. package/src/commands/identity/whoami.ts +34 -0
  42. package/src/commands/idp/add.ts +150 -0
  43. package/src/commands/idp/list.ts +57 -0
  44. package/src/commands/idp/refresh.ts +38 -0
  45. package/src/commands/idp/remove.ts +36 -0
  46. package/src/commands/idp/show.ts +29 -0
  47. package/src/commands/instance/active.ts +64 -0
  48. package/src/commands/instance/bookmark.ts +72 -0
  49. package/src/commands/instance/create.ts +69 -0
  50. package/src/commands/instance/delete.ts +72 -0
  51. package/src/commands/instance/forget.ts +26 -0
  52. package/src/commands/instance/list.ts +149 -0
  53. package/src/commands/instance/status.ts +42 -0
  54. package/src/commands/instance/use.ts +210 -0
  55. package/src/commands/logs.ts +347 -0
  56. package/src/commands/ls.ts +229 -0
  57. package/src/commands/query.ts +32 -0
  58. package/src/commands/setup.ts +54 -0
  59. package/src/commands/status.ts +60 -0
  60. package/src/commands/studio.ts +401 -0
  61. package/src/commands/token.ts +77 -0
  62. package/src/commands/update.ts +267 -0
  63. package/src/commands/use.ts +87 -0
  64. package/src/errors.ts +65 -0
  65. package/src/kernel/__tests__/auth.test.ts +77 -0
  66. package/src/kernel/__tests__/errors.test.ts +43 -0
  67. package/src/kernel/__tests__/remote-routing.test.ts +70 -0
  68. package/src/kernel/auth.ts +234 -0
  69. package/src/kernel/ca-fetch.ts +119 -0
  70. package/src/kernel/client.ts +191 -0
  71. package/src/kernel/errors.ts +280 -0
  72. package/src/kernel/expand.ts +217 -0
  73. package/src/kernel/index.ts +14 -0
  74. package/src/kernel/options.ts +22 -0
  75. package/src/kernel/remote-routing.ts +88 -0
  76. package/src/kernel/run.ts +63 -0
  77. package/src/kernel/types.ts +14 -0
  78. package/src/lib/__tests__/admin-target.test.ts +112 -0
  79. package/src/lib/__tests__/binary.test.ts +56 -0
  80. package/src/lib/__tests__/command-dx.test.ts +58 -0
  81. package/src/lib/__tests__/concurrency.test.ts +62 -0
  82. package/src/lib/__tests__/config.test.ts +53 -0
  83. package/src/lib/__tests__/design.test.ts +99 -0
  84. package/src/lib/__tests__/domain-identity.test.ts +60 -0
  85. package/src/lib/__tests__/format.test.ts +22 -0
  86. package/src/lib/__tests__/fs-atomic.test.ts +104 -0
  87. package/src/lib/__tests__/identity.test.ts +79 -0
  88. package/src/lib/__tests__/idp-session.driver.ts +53 -0
  89. package/src/lib/__tests__/idp-session.test.ts +357 -0
  90. package/src/lib/__tests__/idp.test.ts +385 -0
  91. package/src/lib/__tests__/instance-candidates.test.ts +73 -0
  92. package/src/lib/__tests__/instance-target.test.ts +183 -0
  93. package/src/lib/__tests__/instance.test.ts +136 -0
  94. package/src/lib/__tests__/keys.test.ts +129 -0
  95. package/src/lib/__tests__/local-status.test.ts +202 -0
  96. package/src/lib/__tests__/output.test.ts +150 -0
  97. package/src/lib/__tests__/panel.test.ts +40 -0
  98. package/src/lib/__tests__/port.test.ts +44 -0
  99. package/src/lib/__tests__/prompt.test.ts +25 -0
  100. package/src/lib/__tests__/sdk-deps.test.ts +68 -0
  101. package/src/lib/__tests__/self.test.ts +272 -0
  102. package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
  103. package/src/lib/__tests__/table.test.ts +53 -0
  104. package/src/lib/__tests__/update.test.ts +246 -0
  105. package/src/lib/__tests__/use-target.test.ts +56 -0
  106. package/src/lib/__tests__/validation.test.ts +34 -0
  107. package/src/lib/admin-domain.ts +25 -0
  108. package/src/lib/admin-instance.ts +26 -0
  109. package/src/lib/admin-target.ts +217 -0
  110. package/src/lib/binary.ts +131 -0
  111. package/src/lib/browser.ts +150 -0
  112. package/src/lib/command-dx.ts +161 -0
  113. package/src/lib/concurrency.ts +31 -0
  114. package/src/lib/config.ts +45 -0
  115. package/src/lib/domain-identity.ts +49 -0
  116. package/src/lib/env.ts +49 -0
  117. package/src/lib/format.ts +4 -0
  118. package/src/lib/fs-atomic.ts +126 -0
  119. package/src/lib/identity.ts +256 -0
  120. package/src/lib/idp-session.ts +134 -0
  121. package/src/lib/idp.ts +876 -0
  122. package/src/lib/instance-candidates.ts +49 -0
  123. package/src/lib/instance-target.ts +182 -0
  124. package/src/lib/instance.ts +395 -0
  125. package/src/lib/keys.ts +294 -0
  126. package/src/lib/local-status.ts +152 -0
  127. package/src/lib/log.ts +116 -0
  128. package/src/lib/login-flow.ts +164 -0
  129. package/src/lib/meta.ts +86 -0
  130. package/src/lib/output.ts +222 -0
  131. package/src/lib/panel.ts +61 -0
  132. package/src/lib/paths.ts +11 -0
  133. package/src/lib/port.ts +41 -0
  134. package/src/lib/proc.ts +82 -0
  135. package/src/lib/prompt.ts +136 -0
  136. package/src/lib/provision-instance.ts +170 -0
  137. package/src/lib/sdk-deps.ts +104 -0
  138. package/src/lib/self.ts +166 -0
  139. package/src/lib/skills.ts +171 -0
  140. package/src/lib/table.ts +62 -0
  141. package/src/lib/update.ts +315 -0
  142. package/src/lib/use-target.ts +24 -0
  143. package/src/lib/validation.ts +59 -0
  144. package/src/program.ts +200 -0
  145. package/src/registry.ts +59 -0
  146. package/src/setup/__tests__/util.test.ts +29 -0
  147. package/src/setup/engine.ts +83 -0
  148. package/src/setup/render.ts +109 -0
  149. package/src/setup/steps/admin.ts +78 -0
  150. package/src/setup/steps/agent-browser.ts +81 -0
  151. package/src/setup/steps/auth.ts +54 -0
  152. package/src/setup/steps/domain.ts +68 -0
  153. package/src/setup/steps/index.ts +18 -0
  154. package/src/setup/steps/instance.ts +119 -0
  155. package/src/setup/steps/skills-bridge.ts +70 -0
  156. package/src/setup/steps/skills.ts +59 -0
  157. package/src/setup/types.ts +61 -0
  158. package/src/setup/util.ts +34 -0
  159. package/src/test-utils.ts +18 -0
  160. package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
  161. package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
  162. package/studio/client/dist/index.html +13 -0
  163. package/studio/package.json +62 -0
  164. package/studio/server/agent/ask.ts +68 -0
  165. package/studio/server/agent/bridge-mcp.ts +182 -0
  166. package/studio/server/agent/bridge.ts +188 -0
  167. package/studio/server/agent/claude.ts +666 -0
  168. package/studio/server/agent/mock.ts +186 -0
  169. package/studio/server/agent/prompt.ts +202 -0
  170. package/studio/server/agent/registry.ts +29 -0
  171. package/studio/server/agent/runner.ts +484 -0
  172. package/studio/server/agent/schema-map.ts +112 -0
  173. package/studio/server/agent/types.ts +120 -0
  174. package/studio/server/api.ts +574 -0
  175. package/studio/server/cache.ts +138 -0
  176. package/studio/server/detect.ts +81 -0
  177. package/studio/server/domain.ts +70 -0
  178. package/studio/server/index.ts +136 -0
  179. package/studio/server/introspect/anatomy-extras.ts +398 -0
  180. package/studio/server/introspect/anatomy.ts +108 -0
  181. package/studio/server/introspect/bundle.ts +57 -0
  182. package/studio/server/introspect/core-extractor.ts +119 -0
  183. package/studio/server/introspect/core.ts +44 -0
  184. package/studio/server/introspect/diff.ts +133 -0
  185. package/studio/server/introspect/extractor.ts +102 -0
  186. package/studio/server/introspect/hash.ts +21 -0
  187. package/studio/server/introspect/overlay-tsmorph.ts +874 -0
  188. package/studio/server/introspect/overlay.ts +57 -0
  189. package/studio/server/introspect/runtime.ts +99 -0
  190. package/studio/server/introspect/schema-refs.ts +46 -0
  191. package/studio/server/lifecycle.ts +38 -0
  192. package/studio/server/sse.ts +57 -0
  193. package/studio/server/state/baseline.ts +211 -0
  194. package/studio/server/state/catalog.ts +117 -0
  195. package/studio/server/state/comments.ts +321 -0
  196. package/studio/server/state/context.ts +167 -0
  197. package/studio/server/state/copy.ts +156 -0
  198. package/studio/server/state/create.ts +156 -0
  199. package/studio/server/state/documents.ts +70 -0
  200. package/studio/server/state/env.ts +161 -0
  201. package/studio/server/state/git.ts +75 -0
  202. package/studio/server/state/handoff.ts +55 -0
  203. package/studio/server/state/harness-gateway.ts +181 -0
  204. package/studio/server/state/harness-token.ts +0 -0
  205. package/studio/server/state/instance.ts +244 -0
  206. package/studio/server/state/integrations.ts +55 -0
  207. package/studio/server/state/layout.ts +55 -0
  208. package/studio/server/state/settings.ts +39 -0
  209. package/studio/server/state/store.ts +97 -0
  210. package/studio/server/state/updates.ts +63 -0
  211. package/studio/server/state/usage.ts +37 -0
  212. package/studio/server/state/views.ts +138 -0
  213. package/studio/server/state/visibility.ts +33 -0
  214. package/studio/server/watch.ts +81 -0
  215. package/studio/server/workspace-state.ts +26 -0
  216. package/studio/server/workspace-watch.ts +101 -0
  217. package/studio/shared/types.ts +873 -0
  218. package/studio/tsconfig.json +23 -0
  219. package/tsconfig.json +14 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * agent/mock.ts — a free, deterministic stand-in for a real harness. It exists
3
+ * so the whole live loop (submit → stream events → edit → re-render → reply →
4
+ * merge) can be exercised end-to-end without spending real agent credits.
5
+ *
6
+ * It behaves like a minimal real agent: reads the open threads, makes ONE real
7
+ * edit to the domain schema (so the studio's watch→re-render fires), narrates a
8
+ * few activity events, then returns a final message carrying the same
9
+ * machine-state ```json``` reply block a real agent would emit (so the runner's
10
+ * existing mergeReply path is exercised identically). It writes domain source
11
+ * directly — it is standing in for the external agent actor, not the studio.
12
+ */
13
+ import { readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs'
14
+ import { join } from 'node:path'
15
+
16
+ import type { Comment } from '../../shared/types'
17
+ import type { AgentHarness, AgentTurnInput, AgentTurnResult, AskInput, AskResult } from './types'
18
+
19
+ import { readComments } from '../state/comments'
20
+
21
+ function sleep(ms: number, signal: AbortSignal): Promise<void> {
22
+ return new Promise((resolve) => {
23
+ const t = setTimeout(resolve, ms)
24
+ signal.addEventListener(
25
+ 'abort',
26
+ () => {
27
+ clearTimeout(t)
28
+ resolve()
29
+ },
30
+ { once: true },
31
+ )
32
+ })
33
+ }
34
+
35
+ /** A camelCase identifier derived from free text, for a synthesized prop name. */
36
+ function identFromText(text: string, fallback: string): string {
37
+ const words = text
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9 ]+/g, ' ')
40
+ .trim()
41
+ .split(/\s+/)
42
+ .filter(Boolean)
43
+ .slice(0, 3)
44
+ if (words.length === 0) return fallback
45
+ return words.map((w, i) => (i === 0 ? w : w[0].toUpperCase() + w.slice(1))).join('')
46
+ }
47
+
48
+ /** Insert a new optional prop into the first `props: {` block of the first
49
+ * non-index schema file. Returns the edited file (relative) + prop name. */
50
+ function applyMockEdit(root: string, propName: string): { file: string; prop: string } | null {
51
+ const schemaDir = join(root, 'schema')
52
+ if (!existsSync(schemaDir)) return null
53
+ const files = readdirSync(schemaDir).filter((f) => f.endsWith('.ts') && f !== 'index.ts')
54
+ for (const f of files) {
55
+ const abs = join(schemaDir, f)
56
+ const src = readFileSync(abs, 'utf8')
57
+ const idx = src.indexOf('props: {')
58
+ if (idx < 0) continue
59
+ let prop = propName
60
+ let n = 2
61
+ while (new RegExp(`\\b${prop}\\b\\s*:`).test(src)) prop = `${propName}${n++}`
62
+ const insertAt = idx + 'props: {'.length
63
+ const line = `\n /** Added by the agent in response to a studio comment. */\n ${prop}: z.string().optional(),`
64
+ const next = src.slice(0, insertAt) + line + src.slice(insertAt)
65
+ writeFileSync(abs, next)
66
+ return { file: `schema/${f}`, prop }
67
+ }
68
+ return null
69
+ }
70
+
71
+ export class MockHarness implements AgentHarness {
72
+ id = 'mock'
73
+ label = 'Mock agent (free)'
74
+
75
+ async isAvailable(): Promise<boolean> {
76
+ return true
77
+ }
78
+
79
+ async run(input: AgentTurnInput): Promise<AgentTurnResult> {
80
+ const { root, signal, onEvent } = input
81
+ // test knobs (env): MODE=error|noblock|openreply|badblock|resumefail, DELAY_MS=extra latency for cancel/concurrency tests
82
+ const mode = process.env.DOMAIN_STUDIO_MOCK_MODE || 'normal'
83
+ const extraDelay = Number(process.env.DOMAIN_STUDIO_MOCK_DELAY_MS || 0)
84
+ // 'resumefail' rejects a RESUME (only when a sessionId is passed) so the runner's
85
+ // auto-restart-fresh path can be exercised; the fresh retry (no sessionId) proceeds.
86
+ if (mode === 'resumefail' && input.sessionId) {
87
+ onEvent({ kind: 'status', text: 'resuming…' })
88
+ return {
89
+ sessionId: input.sessionId,
90
+ finalText: '',
91
+ isError: true,
92
+ errorMessage: 'mock: no conversation found with session id',
93
+ resumeRejected: true,
94
+ }
95
+ }
96
+ const store = readComments(root)
97
+ const open = store.comments.filter(
98
+ (c) => c.status === 'open' && c.thread.at(-1)?.role !== 'author',
99
+ )
100
+
101
+ onEvent({ kind: 'status', text: 'session started' })
102
+ await sleep(250, signal)
103
+ if (extraDelay > 0) await sleep(extraDelay, signal)
104
+ if (mode === 'error') throw new Error('mock harness failure (test)')
105
+ onEvent({
106
+ kind: 'thinking',
107
+ text: `Reviewing ${open.length} open thread(s) and the current schema.`,
108
+ })
109
+ await sleep(300, signal)
110
+ onEvent({ kind: 'tool', text: 'Read', tool: 'Read', target: '.domain-studio/comments.json' })
111
+ await sleep(250, signal)
112
+
113
+ // one real edit so the studio re-renders
114
+ const seed = open[0]?.thread.at(-1)?.text ?? 'note'
115
+ const editRes = signal.aborted ? null : applyMockEdit(root, identFromText(seed, 'agentNote'))
116
+ if (editRes) {
117
+ onEvent({ kind: 'tool', text: 'Edit', tool: 'Edit', target: editRes.file })
118
+ await sleep(300, signal)
119
+ }
120
+ onEvent({
121
+ kind: 'message',
122
+ text: editRes
123
+ ? `Added a \`${editRes.prop}\` property to \`${editRes.file}\` and answered the open threads.`
124
+ : 'Answered the open threads.',
125
+ })
126
+
127
+ // final machine-state block (identical shape to a real agent reply)
128
+ const replied: Comment[] = open.map((c) => ({
129
+ ...c,
130
+ status: 'closed',
131
+ thread: [
132
+ ...c.thread,
133
+ {
134
+ id: crypto.randomUUID(),
135
+ role: 'author' as const,
136
+ type: 'text' as const,
137
+ text: editRes
138
+ ? `Done — implemented this by adding \`${editRes.prop}\` to \`${editRes.file}\`. (mock agent)`
139
+ : 'Acknowledged. (mock agent)',
140
+ },
141
+ ],
142
+ }))
143
+ const machine = {
144
+ schemaVersion: store.schemaVersion,
145
+ // 'openreply' leaves threads OPEN with an author entry (clarifying-question loop test)
146
+ comments: replied.map((c) => ({
147
+ id: c.id,
148
+ anchors: c.anchors,
149
+ status: mode === 'openreply' ? 'open' : c.status,
150
+ thread: c.thread,
151
+ })),
152
+ }
153
+ const finalText =
154
+ mode === 'noblock'
155
+ ? 'I reviewed the open threads and made the edit. (no machine-state block — resilience test)'
156
+ : mode === 'badblock'
157
+ ? 'I made the edit.\n\n```json\n{ this is : not valid json, ]\n```\n' // malformed-block resilience test
158
+ : `I reviewed the open threads and made the edit.\n\n\`\`\`json\n${JSON.stringify(machine, null, 2)}\n\`\`\`\n`
159
+
160
+ return {
161
+ sessionId: input.sessionId ?? 'mock-session',
162
+ finalText,
163
+ costUsd: 0,
164
+ numTurns: 1,
165
+ isError: false,
166
+ }
167
+ }
168
+
169
+ /** Fake streamed answer for a side-question (free plumbing test of the Ask loop). */
170
+ async ask(input: AskInput): Promise<AskResult> {
171
+ const forked = input.sessionId ? `(forked from ${input.sessionId.slice(0, 8)}…) ` : '(fresh) '
172
+ const parts = [
173
+ forked,
174
+ 'This is a mock answer to your side question. ',
175
+ 'In a real run, Haiku would answer here from the inherited conversation context.',
176
+ ]
177
+ let text = ''
178
+ for (const p of parts) {
179
+ if (input.signal.aborted) break
180
+ text += p
181
+ input.onDelta(p)
182
+ await sleep(180, input.signal)
183
+ }
184
+ return { text, isError: false }
185
+ }
186
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * agent/prompt.ts — the scaffolding that tells a harness-agnostic agent who it
3
+ * is and exactly how to answer. Two pieces:
4
+ * - the SYSTEM prompt (appended to the harness default) = the durable protocol.
5
+ * - the TURN prompt = the handoff data (context + changes + the threads
6
+ * awaiting a reply + the machine-state block). Reuses buildCopyMarkdown so
7
+ * the live loop and the copy/paste flow stay byte-for-byte consistent.
8
+ */
9
+ import type { Comment, ContextItem, DocMeta, SchemaIR, SchemaOverlay } from '../../shared/types'
10
+
11
+ import { buildCopyMarkdown } from '../state/copy'
12
+ import { describeAnchor, resolveThreadAnchors } from './schema-map'
13
+
14
+ /** The reply protocol — appended to the harness's own system prompt. */
15
+ export function buildSystemPrompt(opts: { bridge: boolean }): string {
16
+ const lines: string[] = [
17
+ 'You are the build agent for an Astrale domain, driven from **Domain Studio** — a local',
18
+ 'GUI where the user pins comment threads onto schema classes, methods, views, data, etc.',
19
+ 'Each thread is a conversation. Your working directory IS the domain repo root.',
20
+ '',
21
+ 'FIRST, read each open thread and judge what it actually wants — editing code is ONE possible',
22
+ 'response, not the default. Match the intent:',
23
+ '- A REQUEST to change something ("add a status enum", "rename X", "implement reservations")',
24
+ ' → make the smallest correct change in code, then reply with what you did.',
25
+ '- A QUESTION or open discussion ("what could we add here?", "should we split this?",',
26
+ ' "is X better than Y?", "thoughts?", "why is …?") → DO NOT touch code. Reply with your',
27
+ ' analysis. When there are a few concrete directions, offer them as OPTIONS (below) so the',
28
+ ' user can choose, then WAIT for their answer before implementing anything.',
29
+ '- AMBIGUOUS, or several reasonable approaches → ask before acting: reply with options rather',
30
+ ' than guessing and editing. Answering now and implementing on a later turn is always fine.',
31
+ 'A comment that asks a question wants a conversation, not a commit. When in doubt, reply first.',
32
+ '',
33
+ 'OFFERING OPTIONS (multiple-choice): when a decision is the user’s to make, give a short menu',
34
+ 'instead of a wall of prose. Pass an `options` array (2–5 short, concrete choices) to',
35
+ 'reply_to_thread (to answer their thread) or raise_question (to open a new one). The user picks',
36
+ 'one or types their own; you see their choice next turn. Keep the surrounding text to one framing line.',
37
+ '',
38
+ 'Operating rules (when a thread genuinely calls for a code change):',
39
+ '- Read the skills under `.agents/skills/` FIRST and follow them: **astrale-domain**',
40
+ ' (schema modeling, handlers, views, deploy/install) before editing schema/handlers,',
41
+ ' and **astrale-cli** when you run the `astrale` CLI. Honor their conventions (edges',
42
+ ' snake_case, compiled key accessors, `::update` drops `z.enum()`, ports/adapters for',
43
+ ' external APIs, idempotent postInstall, colon MethodPaths in postInstall).',
44
+ '- Prefer editing existing',
45
+ ' schema/ runtime/ views/ files over inventing new structure; wire new modules',
46
+ ' EXPLICITLY in domain.ts / schema/index.ts (no folder magic).',
47
+ '- The studio re-renders automatically as you save files — never start, build, or refresh',
48
+ ' anything for the UI to update.',
49
+ '- Sanity-check schema/handler edits with `pnpm typecheck` (or `tsgo --noEmit`).',
50
+ '- You ARE allowed to run the shell. When a thread asks you to **deploy or install**, do',
51
+ ' it yourself: `pnpm prod` (managed deploy + install) or the `astrale` CLI. The user is',
52
+ ' already authenticated (`astrale auth`). After deploying, VERIFY: `curl <svc-url>/meta`',
53
+ ' must name this domain, and a live smoke call (create a node, call a method) should work',
54
+ ' — report the URL and what you verified. Long-running commands are fine.',
55
+ '- SINGLE HEADLESS TURN — you are not a resident session: there is NO background loop and NO',
56
+ ' wake-on-completion. A shell started with run_in_background (or any “notify me when it',
57
+ ' finishes” task) is KILLED a few seconds after this turn ends, and you will NOT be re-invoked',
58
+ ' when it completes. So whenever you must WAIT — a deploy to finish, a service to come up, a',
59
+ ' long build, or polling an endpoint until it is ready — do it SYNCHRONOUSLY in the FOREGROUND',
60
+ ' in THIS turn: block on the command, or run a foreground `until …; do sleep N; done` loop',
61
+ ' (with a sane overall timeout), then act on the result. Never background a wait-and-react task',
62
+ ' and end your turn expecting to be woken — you won’t be.',
63
+ '- Do NOT hand-edit anything under .domain-studio/ (that is the studio’s own state).',
64
+ '',
65
+ 'RAISE DX ISSUES — Astrale runs an in-graph issue tracker (the primary feedback channel). If',
66
+ 'working in this domain costs YOU friction — a confusing or missing Astrale API/error, stale',
67
+ 'docs or skills, or a bug that forced a workaround — FILE a DX issue as part of the task, per',
68
+ 'the **astrale-cli** skill’s “Reporting DX Issues” section: dedup against open issues first, then',
69
+ '`astrale call /:admin.astrale.ai:class.Issue:report kind=<bug|friction|feature> title=… body=…`.',
70
+ 'It is best-effort and non-blocking — never let filing one derail the user’s task — and mention',
71
+ 'the returned id in your reply when you file one.',
72
+ '',
73
+ 'REPLY PROTOCOL — this is how the user sees your answers (they are NOT watching your terminal):',
74
+ '- For EVERY open thread, append exactly one concise {role:"author"} reply: what you changed,',
75
+ ' your answer (with `options` when it is a decision), or a clarifying question.',
76
+ '- If you fully addressed a thread, also set "status":"closed" and add a short "closeNote".',
77
+ '- You MUST end your final message with a fenced ```json``` machine-state block of the SAME',
78
+ ' shape you were given: { "schemaVersion", "comments":[ { "id", "anchors", "status",',
79
+ ' "thread":[ ...every existing entry PLUS your new author entries... ] } ] }. Merge is by id;',
80
+ ' a thread whose last entry is not yours is resent next turn, so always answer every thread.',
81
+ ]
82
+ if (opts.bridge) {
83
+ lines.push(
84
+ '',
85
+ 'PREFERRED CHANNEL — the **domain-studio** MCP tools are connected. Use them as your',
86
+ 'PRIMARY way to reply, so the user sees answers appear live in the threads:',
87
+ ' • list_open_threads — see what to address (ids + anchors).',
88
+ ' • reply_to_thread { commentId, text, resolve?, closeNote?, options? } — answer one thread.',
89
+ ' Pass `options` (2–5 short strings) to offer a multiple-choice decision. Set resolve=true',
90
+ ' ONLY when fully handled — never resolve a question you just asked; wait for the answer.',
91
+ ' • raise_question { ref, text, options? } — open a NEW question thread, optionally with choices.',
92
+ ' • resolve_thread / post_progress — close or narrate.',
93
+ 'When you reply through these tools you do NOT need the final json block — it is only a',
94
+ 'fallback for if a tool call fails. Still answer EVERY open thread one way or the other.',
95
+ )
96
+ }
97
+ lines.push(
98
+ '',
99
+ 'WRITING / VOICE — your reader is usually a BUSINESS user, not an engineer. Write the way',
100
+ 'you would brief a smart colleague who does not code:',
101
+ '- Lead with the outcome in plain language — what now works, what it means for them — not',
102
+ ' the mechanism. Keep sentences short and concrete; one idea per sentence.',
103
+ '- Default to business framing over technical detail. Skip jargon,',
104
+ ' file paths and code unless they are the point. If a technical term is unavoidable, gloss it',
105
+ ' in a few plain words.',
106
+ '- Only go technical when the user does first — match their level. Even then, stay crisp:',
107
+ ' precise and direct, never vague, hand-wavy or padded. No filler, no hedging, no buzzwords.',
108
+ 'Keep prose short. The substance goes in the thread replies and the code.',
109
+ )
110
+ return lines.join('\n')
111
+ }
112
+
113
+ export interface TurnParts {
114
+ origin: string
115
+ root: string
116
+ schemaHash: string
117
+ /** only the threads awaiting an author reply (open, last entry not author) */
118
+ awaitingThreads: Comment[]
119
+ userContext: ContextItem[]
120
+ autoContext: ContextItem[]
121
+ documents: DocMeta[]
122
+ firstTurn: boolean
123
+ /** optional free-text instruction the user typed in the Context composer */
124
+ message?: string
125
+ /** current schema IR (null when deps aren't installed → static fallback) */
126
+ ir: SchemaIR | null
127
+ overlay?: SchemaOverlay
128
+ }
129
+
130
+ /** The per-turn message piped to the harness. */
131
+ export function buildTurnPrompt(parts: TurnParts): string {
132
+ const body = buildCopyMarkdown({
133
+ origin: parts.origin,
134
+ root: parts.root,
135
+ schemaHash: parts.schemaHash,
136
+ openComments: parts.awaitingThreads,
137
+ userContext: parts.userContext,
138
+ autoContext: parts.autoContext,
139
+ documents: parts.documents,
140
+ })
141
+ const hasThreads = parts.awaitingThreads.length > 0
142
+ const header = parts.firstTurn
143
+ ? hasThreads
144
+ ? '> New session. The thread pointers and context below are your orientation — implement the open threads and reply by id.'
145
+ : '> New session. Follow the direct instruction below; use the context below and read schema/ when needed.'
146
+ : hasThreads
147
+ ? '> Follow-up turn in the SAME session. The schema files are current (incl. your prior edits); the threads below were added or updated since your last reply — implement and answer them.'
148
+ : '> Follow-up turn in the SAME session. The schema files are current (incl. your prior edits). Follow the direct instruction below.'
149
+ // Inject only precise thread-anchor pointers. The schema overview is not embedded;
150
+ // the system prompt tells the agent to read/edit schema/ directly when needed.
151
+ const anchors = resolveThreadAnchors(parts.awaitingThreads, parts.overlay)
152
+ const msg = parts.message?.trim()
153
+ const instruction = msg ? ['', '## Direct instruction', '', msg] : []
154
+ return [header, ...instruction, '', anchors, '', body].filter((s) => s !== undefined).join('\n')
155
+ }
156
+
157
+ /** A bare "pick up where you left off" nudge for resuming an interrupted turn.
158
+ * Deliberately empty of handoff data: the resumed session already holds the
159
+ * threads, context and prior edits, so re-sending them would be pure noise — the
160
+ * point of Resume is a seamless continue, not a fresh briefing. */
161
+ export function buildResumePrompt(): string {
162
+ return [
163
+ '> Resuming the SAME session. Your previous turn was cut off when Domain Studio',
164
+ '> restarted — nothing else has changed. Pick up exactly where you left off.',
165
+ '',
166
+ 'Continue and finish what you were doing, then make sure every open thread gets an',
167
+ 'answer through the usual channel (the domain-studio MCP tools, or the final',
168
+ 'machine-state ```json``` block). Keep going from where you stopped — do not restart.',
169
+ ].join('\n')
170
+ }
171
+
172
+ /** The system prompt for a quick Ask side-question — capable, concise, ephemeral. */
173
+ export function buildAskSystemPrompt(): string {
174
+ return [
175
+ 'You are answering a QUICK side question from inside Domain Studio about ONE element',
176
+ 'of an Astrale domain. This is an ephemeral aside, shown in a small popover and then',
177
+ 'discarded — it is NOT part of the main build conversation.',
178
+ '- Answer directly and concisely (usually 1–4 sentences). Lead with the answer.',
179
+ '- You have the same local tool surface and permission mode as the main agent. You may',
180
+ ' inspect files, run commands, use web/search tools if available, and edit files when',
181
+ ' the user explicitly asks for a change. Keep any edits tightly scoped and say what changed.',
182
+ '- No preamble, no machine-state/JSON block, no thread protocol — just the answer.',
183
+ ].join('\n')
184
+ }
185
+
186
+ export interface AskParts {
187
+ anchorRef: string
188
+ excerpt: string
189
+ question: string
190
+ ir: SchemaIR | null
191
+ overlay?: SchemaOverlay
192
+ }
193
+
194
+ /** Compose the Ask turn: a friendly prefix + the injected target context + the question. */
195
+ export function buildAskPrompt(parts: AskParts): string {
196
+ const target = describeAnchor(parts.anchorRef, parts.ir, parts.overlay)
197
+ const named = parts.excerpt && parts.excerpt !== parts.anchorRef ? ` (${parts.excerpt})` : ''
198
+ const lines = [`Quick question about \`${parts.anchorRef}\`${named} in this domain.`]
199
+ if (target) lines.push('', 'Target:', target)
200
+ lines.push('', `Question: ${parts.question.trim()}`)
201
+ return lines.join('\n')
202
+ }
@@ -0,0 +1,29 @@
1
+ import type { AgentHarness } from './types'
2
+
3
+ /**
4
+ * agent/registry.ts — picks the active harness. Default is Claude Code; set
5
+ * DOMAIN_STUDIO_HARNESS=mock for the free deterministic stand-in (tests/dev),
6
+ * or =claude explicitly. New harnesses (codex, …) register here.
7
+ */
8
+ import { ClaudeCodeHarness } from './claude'
9
+ import { MockHarness } from './mock'
10
+
11
+ const harnesses: Record<string, () => AgentHarness> = {
12
+ claude: () => new ClaudeCodeHarness(),
13
+ mock: () => new MockHarness(),
14
+ }
15
+
16
+ let active: AgentHarness | null = null
17
+
18
+ export function getHarness(): AgentHarness {
19
+ if (active) return active
20
+ const want = (process.env.DOMAIN_STUDIO_HARNESS || 'claude').toLowerCase()
21
+ const make = harnesses[want] ?? harnesses.claude
22
+ active = make()
23
+ return active
24
+ }
25
+
26
+ /** All registered harnesses (id + label) — for the (currently locked) UI selector. */
27
+ export function listHarnesses(): { id: string; label: string }[] {
28
+ return Object.entries(harnesses).map(([id, make]) => ({ id, label: make().label }))
29
+ }