@capagents/forgeqe 0.0.0-stage → 1.0.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 (220) hide show
  1. package/HOW_TO_USE.md +1639 -0
  2. package/README.md +338 -2
  3. package/bin/forgeqe.d.ts +3 -0
  4. package/bin/forgeqe.js +188 -0
  5. package/docs/ARCHITECTURE.md +408 -0
  6. package/docs/security-data-flow.html +1870 -0
  7. package/docs/user-guide.html +2094 -0
  8. package/evals/domain/access-role-change.yaml +37 -0
  9. package/evals/domain/banking-payment-transfer.yaml +41 -0
  10. package/evals/domain/gdpr-account-erasure.yaml +30 -0
  11. package/evals/domain/insurance-claim-fnol.yaml +30 -0
  12. package/evals/domain/insurance-policy-renewal.yaml +37 -0
  13. package/evals/domain/lending-loan-application.yaml +36 -0
  14. package/forgeqe-architecture.html +718 -0
  15. package/forgeqe.example.yaml +203 -0
  16. package/knowledge/README.md +18 -0
  17. package/knowledge/business-rules.md +10 -0
  18. package/knowledge/compliance.md +16 -0
  19. package/knowledge/environments.md +5 -0
  20. package/knowledge/glossary.md +5 -0
  21. package/knowledge/product.md +16 -0
  22. package/knowledge/roles.md +9 -0
  23. package/knowledge/test-data.md +7 -0
  24. package/llms.example.json +51 -0
  25. package/package.json +63 -4
  26. package/prompts/README.md +34 -0
  27. package/prompts/api-scenarios.md +21 -0
  28. package/prompts/api-tests.md +39 -0
  29. package/prompts/brain.md +166 -0
  30. package/prompts/browser-capture.md +7 -0
  31. package/prompts/browser-case-report.md +7 -0
  32. package/prompts/browser-checks.md +14 -0
  33. package/prompts/browser-efficiency.md +7 -0
  34. package/prompts/browser-error-recovery.md +15 -0
  35. package/prompts/browser-extra.md +5 -0
  36. package/prompts/browser-goal-verification.md +13 -0
  37. package/prompts/browser-skill-select.md +6 -0
  38. package/prompts/domain-rule.md +13 -0
  39. package/prompts/eval-judge.md +19 -0
  40. package/prompts/explore-charters.md +27 -0
  41. package/prompts/find-tests.md +28 -0
  42. package/prompts/heal.md +40 -0
  43. package/prompts/pr-review.md +29 -0
  44. package/prompts/skill-rule.md +6 -0
  45. package/prompts/test-agent.md +34 -0
  46. package/prompts/test-reviewer.md +30 -0
  47. package/prompts/test-rules.md +28 -0
  48. package/py/forgeqe_engine/__init__.py +3 -0
  49. package/py/forgeqe_engine/__main__.py +3 -0
  50. package/py/forgeqe_engine/brand.py +453 -0
  51. package/py/forgeqe_engine/brand_assets.py +6 -0
  52. package/py/forgeqe_engine/browser.py +196 -0
  53. package/py/forgeqe_engine/capture.py +538 -0
  54. package/py/forgeqe_engine/checks.py +98 -0
  55. package/py/forgeqe_engine/cli.py +188 -0
  56. package/py/forgeqe_engine/diagnostics.py +609 -0
  57. package/py/forgeqe_engine/llm.py +195 -0
  58. package/py/forgeqe_engine/logins.py +251 -0
  59. package/py/forgeqe_engine/loops.py +224 -0
  60. package/py/forgeqe_engine/memory.py +1004 -0
  61. package/py/forgeqe_engine/network.py +244 -0
  62. package/py/forgeqe_engine/prompts.py +20 -0
  63. package/py/forgeqe_engine/replay.py +240 -0
  64. package/py/forgeqe_engine/report.py +173 -0
  65. package/py/forgeqe_engine/service.py +1335 -0
  66. package/py/forgeqe_engine/settings.py +93 -0
  67. package/py/forgeqe_engine/skills.py +88 -0
  68. package/py/forgeqe_engine/summary.py +239 -0
  69. package/py/forgeqe_engine/urls.py +81 -0
  70. package/py/forgeqe_engine/workflow_yaml.py +689 -0
  71. package/skills/dotnet-nunit-playwright/SKILL.md +34 -0
  72. package/skills/dynamics-365/SKILL.md +21 -0
  73. package/skills/playwright-typescript/SKILL.md +35 -0
  74. package/skills/writing-skills/SKILL.md +30 -0
  75. package/src/api/plan.ts +246 -0
  76. package/src/api/run.ts +204 -0
  77. package/src/api/scenario.ts +502 -0
  78. package/src/api/schema.ts +170 -0
  79. package/src/api/spec.ts +624 -0
  80. package/src/api/store.ts +232 -0
  81. package/src/api/traffic.ts +281 -0
  82. package/src/api/types.ts +163 -0
  83. package/src/api/write.ts +272 -0
  84. package/src/auth/signin.ts +494 -0
  85. package/src/auth/store.ts +170 -0
  86. package/src/brain/index.ts +189 -0
  87. package/src/brain/prompt.ts +19 -0
  88. package/src/capabilities/docs.ts +101 -0
  89. package/src/capabilities/guide.ts +455 -0
  90. package/src/capabilities/registry.ts +343 -0
  91. package/src/capabilities/render.ts +106 -0
  92. package/src/capabilities/status.ts +137 -0
  93. package/src/card.ts +11 -0
  94. package/src/cases/ado.ts +325 -0
  95. package/src/cases/api.ts +117 -0
  96. package/src/cases/bugs.ts +223 -0
  97. package/src/cases/confluence.ts +111 -0
  98. package/src/cases/context.ts +223 -0
  99. package/src/cases/dashboards.ts +137 -0
  100. package/src/cases/failure.ts +97 -0
  101. package/src/cases/files.ts +295 -0
  102. package/src/cases/goal.ts +53 -0
  103. package/src/cases/http.ts +111 -0
  104. package/src/cases/index.ts +131 -0
  105. package/src/cases/jira.ts +122 -0
  106. package/src/cases/qtest.ts +108 -0
  107. package/src/cases/quality.ts +190 -0
  108. package/src/cases/rag.ts +100 -0
  109. package/src/cases/readiness.ts +284 -0
  110. package/src/cases/save.ts +149 -0
  111. package/src/cases/sprints.ts +148 -0
  112. package/src/cases/testrail.ts +176 -0
  113. package/src/cases/types.ts +107 -0
  114. package/src/cases/wiki.ts +105 -0
  115. package/src/cases/xray.ts +206 -0
  116. package/src/cases/zephyr.ts +153 -0
  117. package/src/childEnv.ts +86 -0
  118. package/src/cli/args.ts +114 -0
  119. package/src/cli/help.ts +167 -0
  120. package/src/cli/index.ts +1142 -0
  121. package/src/cli/terminal.ts +187 -0
  122. package/src/code/areas.ts +337 -0
  123. package/src/code/attention.ts +214 -0
  124. package/src/code/find.ts +540 -0
  125. package/src/code/handoff.ts +541 -0
  126. package/src/code/history.ts +170 -0
  127. package/src/code/impact.ts +231 -0
  128. package/src/code/index.ts +34 -0
  129. package/src/code/livebrowser.ts +67 -0
  130. package/src/code/loop.ts +603 -0
  131. package/src/code/notes.ts +75 -0
  132. package/src/code/profile.ts +226 -0
  133. package/src/code/prompt.ts +303 -0
  134. package/src/code/prreview.ts +649 -0
  135. package/src/code/prtests.ts +103 -0
  136. package/src/code/replay.ts +772 -0
  137. package/src/code/review.ts +202 -0
  138. package/src/code/runtime.ts +757 -0
  139. package/src/code/slices.ts +159 -0
  140. package/src/code/testrun.ts +187 -0
  141. package/src/code/traceability.ts +100 -0
  142. package/src/code/worktree.ts +153 -0
  143. package/src/commands.ts +101 -0
  144. package/src/config.ts +970 -0
  145. package/src/detail.ts +65 -0
  146. package/src/domain.ts +19 -0
  147. package/src/engine/bridge.ts +359 -0
  148. package/src/engine/mock.ts +102 -0
  149. package/src/engine/setup.ts +298 -0
  150. package/src/entra.ts +154 -0
  151. package/src/env.ts +24 -0
  152. package/src/evals/domain.ts +338 -0
  153. package/src/explore/charters.ts +151 -0
  154. package/src/explore/run.ts +280 -0
  155. package/src/explore/story.ts +106 -0
  156. package/src/guard.ts +51 -0
  157. package/src/heal/loop.ts +357 -0
  158. package/src/heal/prompt.ts +143 -0
  159. package/src/heal/report.ts +95 -0
  160. package/src/index.ts +12 -0
  161. package/src/init.ts +102 -0
  162. package/src/knowledge.ts +114 -0
  163. package/src/legacyEnv.ts +9 -0
  164. package/src/llm.ts +61 -0
  165. package/src/locale.ts +25 -0
  166. package/src/logins.ts +148 -0
  167. package/src/managed.ts +101 -0
  168. package/src/mcp/apitools.ts +335 -0
  169. package/src/mcp/captools.ts +25 -0
  170. package/src/mcp/casetools.ts +223 -0
  171. package/src/mcp/commands.ts +421 -0
  172. package/src/mcp/exploretools.ts +129 -0
  173. package/src/mcp/external.ts +172 -0
  174. package/src/mcp/git.ts +78 -0
  175. package/src/mcp/notifytools.ts +49 -0
  176. package/src/mcp/prtools.ts +261 -0
  177. package/src/mcp/qualitytools.ts +154 -0
  178. package/src/mcp/repotests.ts +286 -0
  179. package/src/mcp/server.ts +234 -0
  180. package/src/mcp/tools.ts +810 -0
  181. package/src/memory.ts +250 -0
  182. package/src/money.ts +39 -0
  183. package/src/monitor/alerts.ts +94 -0
  184. package/src/monitor/background.ts +68 -0
  185. package/src/monitor/run.ts +263 -0
  186. package/src/monitor/state.ts +105 -0
  187. package/src/notify/events.ts +144 -0
  188. package/src/notify/notice.ts +177 -0
  189. package/src/prices.ts +175 -0
  190. package/src/print.ts +54 -0
  191. package/src/privacy.ts +35 -0
  192. package/src/prompts.ts +165 -0
  193. package/src/quality/budgets.ts +249 -0
  194. package/src/quality/history.ts +149 -0
  195. package/src/quality/visual.ts +201 -0
  196. package/src/report/data.ts +475 -0
  197. package/src/report/html.ts +106 -0
  198. package/src/report/template.html +84 -0
  199. package/src/run.ts +243 -0
  200. package/src/sessionLog.ts +216 -0
  201. package/src/sessionLogins.ts +94 -0
  202. package/src/skills/registry.ts +217 -0
  203. package/src/skills/scaffold.ts +65 -0
  204. package/src/skills/select.ts +37 -0
  205. package/src/skills/view.ts +44 -0
  206. package/src/suite/report.ts +135 -0
  207. package/src/suite/run.ts +303 -0
  208. package/src/summary.ts +188 -0
  209. package/src/tools.ts +130 -0
  210. package/src/tui/App.tsx +983 -0
  211. package/src/tui/clipboard.ts +36 -0
  212. package/src/tui/components.tsx +263 -0
  213. package/src/tui/dialogs.ts +65 -0
  214. package/src/tui/main.tsx +20 -0
  215. package/src/tui/text.ts +28 -0
  216. package/src/tui/theme.ts +104 -0
  217. package/src/tui/transcript.tsx +344 -0
  218. package/src/types.ts +136 -0
  219. package/src/update.ts +311 -0
  220. package/src/usage.ts +158 -0
@@ -0,0 +1,234 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto"
2
+
3
+ import { logEvent } from "../sessionLog.ts"
4
+ import type { McpTool, ToolContext, ToolEvent } from "./tools.ts"
5
+
6
+ type Json = Record<string, any>
7
+ type Message = { jsonrpc: "2.0"; id?: string | number | null; method?: string; params?: Json; result?: unknown; error?: unknown }
8
+ type Send = (message: Json) => void
9
+
10
+ const PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"]
11
+ const PROGRESS_EVERY_MS = 10_000
12
+
13
+ /** The MCP server name ForgeQE's tools get in the agent runtime, so they appear as forgeqe_* tools. */
14
+ export const MCP_NAME = "forgeqe"
15
+ /** Tool calls may take as long as a whole suite. */
16
+ export const TOOL_TIMEOUT_MS = 6 * 60 * 60 * 1000
17
+
18
+ export type McpServerOptions = {
19
+ tools: McpTool[]
20
+ /** Read on every call, so the shell's current profile and headed setting apply. */
21
+ context: () => ToolContext
22
+ instructions?: () => string
23
+ version?: string
24
+ progressMs?: number
25
+ }
26
+
27
+ /** JSON-RPC dispatch for MCP: initialize, ping, tools/list, tools/call, and cancellation. */
28
+ export function createMcpServer(opts: McpServerOptions) {
29
+ const running = new Map<string | number, AbortController>()
30
+
31
+ async function callTool(params: Json, id: string | number, send: Send, outer?: AbortSignal): Promise<Json> {
32
+ const tool = opts.tools.find((t) => t.name === params.name)
33
+ if (!tool) return { content: [{ type: "text", text: `Unknown tool ${params.name}` }], isError: true }
34
+ const base = opts.context()
35
+ const controller = new AbortController()
36
+ const abort = () => controller.abort()
37
+ base.signal?.addEventListener("abort", abort, { once: true })
38
+ outer?.addEventListener("abort", abort, { once: true })
39
+ running.set(id, controller)
40
+ const token = params._meta?.progressToken
41
+ let tick = 0
42
+ const timer = token !== undefined ? setInterval(() => send({ jsonrpc: "2.0", method: "notifications/progress", params: { progressToken: token, progress: ++tick, message: `${tool.name} running` } }), opts.progressMs || PROGRESS_EVERY_MS) : null
43
+ const started = Date.now()
44
+ logEvent("tool.call", { tool: tool.name, arguments: params.arguments || {} })
45
+ const finish = (result: Json) => {
46
+ logEvent("tool.result", { tool: tool.name, ms: Date.now() - started, error: Boolean(result.isError), text: result.content?.[0]?.text })
47
+ return result
48
+ }
49
+ try {
50
+ const emit = base.emit
51
+ ? (e: ToolEvent) => {
52
+ // Browser run events are logged by the engine bridge as engine.event.
53
+ if (e.type !== "run-event") logEvent("tool.event", { tool: tool.name, ...e })
54
+ base.emit?.(e)
55
+ }
56
+ : undefined
57
+ const out = await tool.run(params.arguments || {}, { ...base, emit, signal: controller.signal })
58
+ if (controller.signal.aborted) return finish({ content: [{ type: "text", text: "Stopped by the user." }], isError: true })
59
+ return finish({ content: [{ type: "text", text: typeof out === "string" ? out : JSON.stringify(out, null, 2) }] })
60
+ } catch (err) {
61
+ return finish({ content: [{ type: "text", text: controller.signal.aborted ? "Stopped by the user." : (err as Error).message || String(err) }], isError: true })
62
+ } finally {
63
+ if (timer) clearInterval(timer)
64
+ running.delete(id)
65
+ base.signal?.removeEventListener("abort", abort)
66
+ outer?.removeEventListener("abort", abort)
67
+ }
68
+ }
69
+
70
+ /** The response for a request, or null for notifications and responses. */
71
+ async function handle(msg: Message, send: Send, signal?: AbortSignal): Promise<Json | null> {
72
+ if (!msg.method) return null
73
+ if (msg.id === undefined || msg.id === null) {
74
+ if (msg.method === "notifications/cancelled") running.get(msg.params?.requestId)?.abort()
75
+ return null
76
+ }
77
+ const ok = (result: unknown) => ({ jsonrpc: "2.0", id: msg.id, result })
78
+ switch (msg.method) {
79
+ case "initialize": {
80
+ const asked = String(msg.params?.protocolVersion || "")
81
+ return ok({
82
+ protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
83
+ capabilities: { tools: { listChanged: false } },
84
+ serverInfo: { name: "forgeqe", version: opts.version || "0" },
85
+ ...(opts.instructions ? { instructions: opts.instructions() } : {}),
86
+ })
87
+ }
88
+ case "ping":
89
+ return ok({})
90
+ case "tools/list":
91
+ return ok({ tools: opts.tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) })
92
+ case "tools/call":
93
+ return ok(await callTool(msg.params || {}, msg.id, send, signal))
94
+ default:
95
+ return { jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } }
96
+ }
97
+ }
98
+
99
+ return { handle, abortAll: () => running.forEach((c) => c.abort()) }
100
+ }
101
+
102
+ export type McpServer = ReturnType<typeof createMcpServer>
103
+
104
+ /** The bearer token check, in constant time. */
105
+ export function tokenMatches(header: string | null, token: string): boolean {
106
+ const digest = (s: string) => createHash("sha256").update(s).digest()
107
+ return timingSafeEqual(digest(header || ""), digest(`Bearer ${token}`))
108
+ }
109
+
110
+ /**
111
+ * A browser page may not call the tools (DNS rebinding): requests that carry an Origin must come from this machine.
112
+ * MCP clients that are not browsers send no Origin.
113
+ */
114
+ export function originAllowed(origin: string | null): boolean {
115
+ if (!origin) return true
116
+ try {
117
+ return ["127.0.0.1", "localhost", "[::1]"].includes(new URL(origin).hostname)
118
+ } catch {
119
+ return false
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Streamable HTTP on 127.0.0.1 with a bearer token. Tool calls answer as an event stream so progress notifications
125
+ * and keep-alives flow while a browser run takes minutes; everything else answers as JSON.
126
+ */
127
+ export function serveHttp(server: McpServer, token: string): { url: string; stop(): void } {
128
+ const http = Bun.serve({
129
+ hostname: "127.0.0.1",
130
+ port: 0,
131
+ idleTimeout: 0,
132
+ async fetch(req) {
133
+ if (new URL(req.url).pathname !== "/mcp") return new Response("Not found", { status: 404 })
134
+ if (!originAllowed(req.headers.get("origin"))) return new Response("Forbidden", { status: 403 })
135
+ if (!tokenMatches(req.headers.get("authorization"), token)) return new Response("Unauthorized", { status: 401 })
136
+ if (req.method === "DELETE") return new Response(null, { status: 200 })
137
+ if (req.method !== "POST") return new Response(null, { status: 405, headers: { Allow: "POST, DELETE" } })
138
+ let body: Message | Message[]
139
+ try {
140
+ body = await req.json()
141
+ } catch {
142
+ return Response.json({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }, { status: 400 })
143
+ }
144
+ const batch = Array.isArray(body) ? body : [body]
145
+ const requests = batch.filter((m) => m.method && m.id !== undefined && m.id !== null)
146
+ if (!requests.length) {
147
+ for (const m of batch) await server.handle(m, () => {})
148
+ return new Response(null, { status: 202 })
149
+ }
150
+ if (!requests.some((m) => m.method === "tools/call")) {
151
+ const out = (await Promise.all(batch.map((m) => server.handle(m, () => {})))).filter(Boolean)
152
+ return Response.json(Array.isArray(body) ? out : out[0])
153
+ }
154
+ const enc = new TextEncoder()
155
+ const stream = new ReadableStream({
156
+ async start(controller) {
157
+ let open = true
158
+ const write = (s: string) => {
159
+ if (!open) return
160
+ try {
161
+ controller.enqueue(enc.encode(s))
162
+ } catch {
163
+ open = false
164
+ }
165
+ }
166
+ const send: Send = (m) => write(`event: message\ndata: ${JSON.stringify(m)}\n\n`)
167
+ const keepAlive = setInterval(() => write(": keep-alive\n\n"), 15_000)
168
+ try {
169
+ await Promise.all(
170
+ batch.map(async (m) => {
171
+ const res = await server.handle(m, send, req.signal)
172
+ if (res) send(res)
173
+ }),
174
+ )
175
+ } finally {
176
+ clearInterval(keepAlive)
177
+ open = false
178
+ try {
179
+ controller.close()
180
+ } catch {}
181
+ }
182
+ },
183
+ })
184
+ return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } })
185
+ },
186
+ })
187
+ return { url: `http://127.0.0.1:${http.port}/mcp`, stop: () => http.stop(true) }
188
+ }
189
+
190
+ /**
191
+ * ForgeQE's tools on 127.0.0.1 behind a random bearer token, for one agent runtime session: `entry` goes under `mcp` in
192
+ * its config. Tool calls may take as long as a whole suite; progress notifications keep the connection busy meanwhile.
193
+ */
194
+ export function localToolServer(opts: McpServerOptions & { timeoutMs: number }): { server: McpServer; entry: Record<string, unknown>; stop(): void } {
195
+ const server = createMcpServer(opts)
196
+ const token = randomBytes(24).toString("hex")
197
+ const http = serveHttp(server, token)
198
+ return {
199
+ server,
200
+ entry: { type: "remote", url: http.url, headers: { Authorization: `Bearer ${token}` }, oauth: false, enabled: true, timeout: opts.timeoutMs },
201
+ stop: () => http.stop(),
202
+ }
203
+ }
204
+
205
+ /** Newline-delimited JSON-RPC on stdin/stdout, for MCP clients that start `forgeqe mcp` themselves. */
206
+ export async function serveStdio(server: McpServer, input: ReadableStream<Uint8Array> = Bun.stdin.stream()): Promise<void> {
207
+ const send: Send = (m) => process.stdout.write(`${JSON.stringify(m)}\n`)
208
+ const dec = new TextDecoder()
209
+ let buf = ""
210
+ const pending = new Set<Promise<void>>()
211
+ for await (const chunk of input) {
212
+ buf += dec.decode(chunk, { stream: true })
213
+ let nl: number
214
+ while ((nl = buf.indexOf("\n")) >= 0) {
215
+ const line = buf.slice(0, nl).trim()
216
+ buf = buf.slice(nl + 1)
217
+ if (!line) continue
218
+ let msg: Message
219
+ try {
220
+ msg = JSON.parse(line)
221
+ } catch {
222
+ send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } })
223
+ continue
224
+ }
225
+ const p = server.handle(msg, send).then((res) => {
226
+ if (res) send(res)
227
+ })
228
+ pending.add(p)
229
+ void p.finally(() => pending.delete(p))
230
+ }
231
+ }
232
+ server.abortAll()
233
+ await Promise.allSettled([...pending])
234
+ }