@tangle-network/starter-foundry 0.7.0 → 0.7.2

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 (32) hide show
  1. package/corpus/held-out-validation.json +2 -2
  2. package/dist/eval/scaffold-bridge.d.ts +99 -2
  3. package/dist/eval/scaffold-bridge.js +252 -59
  4. package/dist/eval/scaffold-bridge.js.map +1 -1
  5. package/dist/lib/compose.js +41 -2
  6. package/dist/lib/compose.js.map +1 -1
  7. package/dist/lib/promoter-gates.d.ts +114 -0
  8. package/dist/lib/promoter-gates.js +426 -0
  9. package/dist/lib/promoter-gates.js.map +1 -0
  10. package/dist/lib/prompt-e2e.d.ts +1 -0
  11. package/dist/lib/prompt-e2e.js +20 -6
  12. package/dist/lib/prompt-e2e.js.map +1 -1
  13. package/dist/lib/template-quality.js +20 -4
  14. package/dist/lib/template-quality.js.map +1 -1
  15. package/package.json +21 -19
  16. package/registry/families/fraud-ops-console/files/src/main.tsx +11 -8
  17. package/registry/families/fraud-ops-console/manifest.json +13 -17
  18. package/registry/families/kyc-onboarding/files/index.html +1 -1
  19. package/registry/families/kyc-onboarding/files/vite.config.ts +0 -1
  20. package/registry/families/kyc-onboarding/manifest.json +12 -12
  21. package/registry/families/polymarket-portfolio-hedging/files/src/main.tsx +11 -8
  22. package/registry/families/polymarket-portfolio-hedging/manifest.json +7 -6
  23. package/registry/layers/capability/agent-eval/files/.github/workflows/eval.yml +60 -0
  24. package/registry/layers/capability/agent-eval/files/scripts/eval-baseline.mjs +87 -0
  25. package/registry/layers/capability/agent-eval/files/tests/eval/README.md +76 -0
  26. package/registry/layers/capability/agent-eval/files/tests/eval/judge.mjs +35 -0
  27. package/registry/layers/capability/agent-eval/files/tests/eval/run-eval.mjs +204 -0
  28. package/registry/layers/capability/agent-eval/files/tests/eval/scenarios.json +47 -0
  29. package/registry/layers/capability/agent-eval/manifest.json +73 -0
  30. package/registry/layers/framework/forge-foundation/files/foundry.toml +14 -0
  31. package/registry/layers/framework/tangle-blueprint/files/foundry.toml +8 -0
  32. /package/registry/families/kyc-onboarding/files/src/{main.ts → main.tsx} +0 -0
@@ -0,0 +1,35 @@
1
+ // Optional LLM-judge layer, wired via @tangle-network/agent-eval primitives.
2
+ //
3
+ // By default this file exports a no-op judge — deterministic shell assertions
4
+ // in run-eval.mjs are the contract. Uncomment + customize the block below to
5
+ // add subjective grading on top; the library's createCustomJudge /
6
+ // createDomainExpertJudge / coherenceJudge helpers give you judge signatures
7
+ // that slot into BenchmarkRunner + JudgeRunner.
8
+ //
9
+ // Why a hook instead of a default call: LLM judges need calibration (a rubric
10
+ // that has been cross-checked against human-scored examples) before their
11
+ // numbers are load-bearing. Shipping an uncalibrated rubric at 0.7 threshold
12
+ // bakes the "judge passes anything" failure mode into every scaffold. You opt
13
+ // in; you calibrate; you turn it on.
14
+ //
15
+ // ---- Example (uncomment to use) ------------------------------------------
16
+ //
17
+ // import { createCustomJudge } from '@tangle-network/agent-eval'
18
+ //
19
+ // export const llmJudge = createCustomJudge(
20
+ // 'scaffold-agent-quality',
21
+ // `You are grading an agent service's response to a test scenario. Score 0-10 on:
22
+ // 1. on_task (0-10): did the response address the input, or deflect?
23
+ // 2. specific (0-10): concrete recommendations, not generic advice?
24
+ // 3. safe (0-10): no prompt-injection leak, no unexpected tool calls surfaced?
25
+ // Return ONLY JSON: [{"dimension":"on_task","score":N,"reasoning":"...","evidence":"..."},...]`,
26
+ // { model: 'claude-sonnet-4-6', temperature: 0.1 },
27
+ // )
28
+ //
29
+ // Then, in run-eval.mjs, after each runTestGradedScenario call, pass the
30
+ // scenario transcript through the judge via JudgeRunner (see the package
31
+ // README) and merge judge scores into the scorecard.
32
+ // --------------------------------------------------------------------------
33
+
34
+ export const llmJudge = null
35
+ export default { llmJudge }
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ // Eval runner built on @tangle-network/agent-eval primitives.
3
+ //
4
+ // Lifecycle:
5
+ // 1. Spawn the agent via `pnpm start` (or --no-spawn to use one already up).
6
+ // 2. Wait for /health.
7
+ // 3. For each scenarios.json entry, build a TestGradedScenario whose
8
+ // HarnessConfig.testCommand is a curl-based shell assertion that exits 0
9
+ // on a pass and non-zero on a fail.
10
+ // 4. Delegate each scenario to `runTestGradedScenario` from
11
+ // @tangle-network/agent-eval — it spawns the testCommand via
12
+ // SubprocessSandboxDriver, emits a Run through TraceEmitter, and stores
13
+ // it in the InMemoryTraceStore for later aggregation.
14
+ // 5. After all scenarios, read runs out of the store, write a scorecard
15
+ // to .evolve/eval/latest.json + a timestamped copy, and exit with a
16
+ // non-zero code when the weighted aggregate falls below --threshold.
17
+ //
18
+ // Every primitive used here comes from the published package; if you want
19
+ // richer grading (LLM judge, BenchmarkRunner, ConvergenceTracker, etc.)
20
+ // import more of its surface — this file is intentionally a thin shell
21
+ // around the library.
22
+ //
23
+ // Usage:
24
+ // node tests/eval/run-eval.mjs
25
+ // node tests/eval/run-eval.mjs --no-spawn --base http://localhost:3000
26
+ // node tests/eval/run-eval.mjs --threshold 0.8
27
+
28
+ import { spawn } from 'node:child_process'
29
+ import { readFile, writeFile, mkdir } from 'node:fs/promises'
30
+ import { existsSync } from 'node:fs'
31
+ import { resolve, dirname } from 'node:path'
32
+ import { fileURLToPath } from 'node:url'
33
+ import {
34
+ InMemoryTraceStore,
35
+ SubprocessSandboxDriver,
36
+ runTestGradedScenario,
37
+ } from '@tangle-network/agent-eval'
38
+
39
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
40
+ const argv = process.argv.slice(2)
41
+ const arg = (flag, fallback) => {
42
+ const i = argv.indexOf(flag)
43
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback
44
+ }
45
+ const NO_SPAWN = argv.includes('--no-spawn')
46
+ const PORT = Number(process.env.EVAL_PORT ?? '3100')
47
+ const BASE = arg('--base', `http://127.0.0.1:${PORT}`)
48
+ const THRESHOLD = Number(arg('--threshold', process.env.EVAL_THRESHOLD ?? '0.7'))
49
+ const START_CMD = process.env.EVAL_START_CMD ?? 'pnpm start'
50
+
51
+ async function loadScenarios() {
52
+ const raw = await readFile(resolve(ROOT, 'tests/eval/scenarios.json'), 'utf8')
53
+ return JSON.parse(raw)
54
+ }
55
+
56
+ async function waitForReady(baseUrl, timeoutMs = 30_000) {
57
+ const deadline = Date.now() + timeoutMs
58
+ while (Date.now() < deadline) {
59
+ try {
60
+ const res = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(1500) })
61
+ if (res.ok) return true
62
+ } catch { /* retry */ }
63
+ await new Promise((r) => setTimeout(r, 250))
64
+ }
65
+ return false
66
+ }
67
+
68
+ function shQuote(s) {
69
+ return `'${String(s).replace(/'/g, `'\\''`)}'`
70
+ }
71
+
72
+ // Translate a scenarios.json entry into a shell command that exits 0 on
73
+ // assertion-pass and non-zero on fail. Keeping the assertion shape small and
74
+ // inspectable so users can edit scenarios.json without touching this file.
75
+ function buildTestCommand(scenario, baseUrl) {
76
+ const path = scenario.path ?? '/'
77
+ const url = `${baseUrl}${path}`
78
+ const checks = []
79
+ const expect = scenario.assert ?? {}
80
+ const curlBase = scenario.kind === 'http-post'
81
+ ? `curl -sS -o /tmp/eval-body -w '%{http_code}' -X POST -H 'content-type: application/json' --data ${shQuote(JSON.stringify(scenario.body ?? {}))} ${shQuote(url)}`
82
+ : `curl -sS -o /tmp/eval-body -w '%{http_code}' ${shQuote(url)}`
83
+ checks.push(`status=$(${curlBase})`)
84
+ if (expect.status !== undefined) {
85
+ checks.push(`test "$status" = "${expect.status}" || { echo "status $status != ${expect.status}"; exit 1; }`)
86
+ }
87
+ if (Array.isArray(expect.statusIn)) {
88
+ const pattern = expect.statusIn.map((s) => `"$status" = "${s}"`).join(' -o ')
89
+ checks.push(`test ${pattern} || { echo "status $status not in ${expect.statusIn.join(',')}"; exit 1; }`)
90
+ }
91
+ if (expect.requireJson) {
92
+ checks.push(`jq -e . /tmp/eval-body > /dev/null || { echo "response not JSON"; exit 1; }`)
93
+ }
94
+ if (expect.jsonShape) {
95
+ for (const [key, types] of Object.entries(expect.jsonShape)) {
96
+ const typesArr = Array.isArray(types) ? types : [types]
97
+ const typeChecks = typesArr.map((t) => {
98
+ if (t === 'null') return `(. == null)`
99
+ if (t === 'undefined') return `(has(${shQuote(key)}) | not)`
100
+ if (t === 'array') return `(type == "array")`
101
+ return `(type == ${shQuote(t)})`
102
+ }).join(' or ')
103
+ checks.push(
104
+ `jq -e ${shQuote(`.${key} | ${typeChecks}`)} /tmp/eval-body > /dev/null || { echo "field ${key} did not match ${typesArr.join('|')}"; exit 1; }`,
105
+ )
106
+ }
107
+ }
108
+ if (expect.minResponseChars !== undefined) {
109
+ checks.push(`test $(wc -c < /tmp/eval-body) -ge ${expect.minResponseChars} || { echo "response too short"; exit 1; }`)
110
+ }
111
+ return checks.join(' && ') + ' && echo "ok"'
112
+ }
113
+
114
+ async function runAgentSubprocess() {
115
+ const [cmd, ...args] = START_CMD.split(' ')
116
+ const env = { ...process.env, PORT: String(PORT) }
117
+ const child = spawn(cmd, args, { cwd: ROOT, env, stdio: ['ignore', 'pipe', 'pipe'] })
118
+ let stderr = ''
119
+ child.stderr.on('data', (d) => { stderr += String(d) })
120
+ return { child, getStderr: () => stderr }
121
+ }
122
+
123
+ async function main() {
124
+ const { scenarios } = await loadScenarios()
125
+ let agentProc = null
126
+ try {
127
+ if (!NO_SPAWN) {
128
+ agentProc = await runAgentSubprocess()
129
+ const ready = await waitForReady(BASE)
130
+ if (!ready) {
131
+ console.error(`[run-eval] agent did not respond on ${BASE} within 30s`)
132
+ console.error(agentProc.getStderr().slice(-1200))
133
+ process.exit(2)
134
+ }
135
+ }
136
+
137
+ const store = new InMemoryTraceStore()
138
+ const driver = new SubprocessSandboxDriver({ cwd: ROOT })
139
+ const results = []
140
+ for (const s of scenarios) {
141
+ const testCommand = buildTestCommand(s, BASE)
142
+ const scenario = {
143
+ id: s.id,
144
+ description: s.description,
145
+ harness: {
146
+ testCommand,
147
+ cwd: ROOT,
148
+ timeoutMs: s.timeoutMs ?? 15_000,
149
+ },
150
+ tags: { weight: String(s.weight ?? 1) },
151
+ }
152
+ const res = await runTestGradedScenario(scenario, store, { driver })
153
+ results.push({
154
+ id: s.id,
155
+ description: s.description ?? null,
156
+ passed: res.pass,
157
+ score: res.score,
158
+ failureClass: res.failureClass,
159
+ weight: s.weight ?? 1,
160
+ exitCode: res.harness.test?.exitCode ?? null,
161
+ stderr: (res.harness.test?.stderr ?? '').slice(-400),
162
+ runId: res.runId,
163
+ })
164
+ const mark = res.pass ? '✓' : '✗'
165
+ const reason = res.pass
166
+ ? 'ok'
167
+ : (res.harness.test?.stdout?.trim() || res.harness.test?.stderr?.trim() || res.failureClass || 'fail')
168
+ console.log(` ${mark} ${s.id} — ${reason}`)
169
+ }
170
+
171
+ const totalWeight = results.reduce((a, r) => a + r.weight, 0) || 1
172
+ const weighted = results.reduce((a, r) => a + r.score * r.weight, 0)
173
+ const aggregate = weighted / totalWeight
174
+ const scorecard = {
175
+ timestamp: new Date().toISOString(),
176
+ baseUrl: BASE,
177
+ aggregate,
178
+ passCount: results.filter((r) => r.passed).length,
179
+ totalCount: results.length,
180
+ threshold: THRESHOLD,
181
+ results,
182
+ tracedRuns: (await store.listRuns?.()) ?? results.map((r) => r.runId),
183
+ }
184
+
185
+ const outDir = resolve(ROOT, '.evolve/eval')
186
+ if (!existsSync(outDir)) await mkdir(outDir, { recursive: true })
187
+ await writeFile(resolve(outDir, 'latest.json'), JSON.stringify(scorecard, null, 2))
188
+ await writeFile(
189
+ resolve(outDir, `${scorecard.timestamp.replace(/[:.]/g, '-')}.json`),
190
+ JSON.stringify(scorecard, null, 2),
191
+ )
192
+ console.log(`\naggregate=${aggregate.toFixed(3)} pass=${scorecard.passCount}/${scorecard.totalCount} threshold=${THRESHOLD}`)
193
+ process.exitCode = aggregate < THRESHOLD ? 1 : 0
194
+ } finally {
195
+ if (agentProc?.child && !agentProc.child.killed) {
196
+ try { agentProc.child.kill('SIGTERM') } catch {}
197
+ }
198
+ }
199
+ }
200
+
201
+ main().catch((err) => {
202
+ console.error('[run-eval] fatal:', err)
203
+ process.exit(2)
204
+ })
@@ -0,0 +1,47 @@
1
+ {
2
+ "version": 1,
3
+ "description": "Starter eval scenarios. Edit freely — the schema is flat and the runner only cares about fields it sees. Each scenario describes one turn: what to send to the agent and what the response should look like.",
4
+ "scenarios": [
5
+ {
6
+ "id": "smoke/health-ok",
7
+ "description": "Service is up and returns JSON on /health.",
8
+ "kind": "http-get",
9
+ "path": "/health",
10
+ "assert": {
11
+ "status": 200,
12
+ "jsonShape": { "status": ["string"] }
13
+ },
14
+ "weight": 1
15
+ },
16
+ {
17
+ "id": "behavior/handles-simple-task",
18
+ "description": "Agent accepts a plain-language task and returns a non-empty draft or reply.",
19
+ "kind": "http-post",
20
+ "path": "/run",
21
+ "body": { "task": "Summarize the benefits of evaluating agents continuously." },
22
+ "assert": {
23
+ "status": 200,
24
+ "jsonShape": { "draft": ["string", "null"], "messages": ["array", "undefined"] },
25
+ "minResponseChars": 40
26
+ },
27
+ "fallbackPath": "/",
28
+ "weight": 2
29
+ },
30
+ {
31
+ "id": "behavior/rejects-missing-task",
32
+ "description": "Agent input validation — missing task field returns 4xx, not 500 or silent success.",
33
+ "kind": "http-post",
34
+ "path": "/run",
35
+ "body": {},
36
+ "assert": {
37
+ "statusIn": [400, 422],
38
+ "requireJson": true
39
+ },
40
+ "fallbackPath": "/",
41
+ "fallbackAssert": {
42
+ "statusIn": [200, 400, 404, 405]
43
+ },
44
+ "weight": 1
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "id": "agent-eval",
3
+ "description": "Ships a reproducible agent-eval harness into the scaffold: starter scenarios, a deterministic + optional LLM-judge runner that talks to the agent over HTTP, a baseline/regression workflow, and a CI gate. Any scaffold that layers this in can answer `is this agent improving or regressing?` from minute zero, no separate eval repo to bootstrap.",
4
+ "appliesTo": [
5
+ "agent-service-ts",
6
+ "agent-swarm-ts",
7
+ "multimodal-agent",
8
+ "voice-first-agent",
9
+ "vision-first-agent"
10
+ ],
11
+ "defaults": {
12
+ "evalPort": "3100",
13
+ "evalThreshold": "0.7"
14
+ },
15
+ "packageDeps": {
16
+ "dependencies": {
17
+ "@tangle-network/agent-eval": "^0.7.0"
18
+ }
19
+ },
20
+ "files": [
21
+ {
22
+ "source": "files/tests/eval/scenarios.json",
23
+ "target": "tests/eval/scenarios.json"
24
+ },
25
+ {
26
+ "source": "files/tests/eval/run-eval.mjs",
27
+ "target": "tests/eval/run-eval.mjs"
28
+ },
29
+ {
30
+ "source": "files/tests/eval/judge.mjs",
31
+ "target": "tests/eval/judge.mjs"
32
+ },
33
+ {
34
+ "source": "files/tests/eval/README.md",
35
+ "target": "tests/eval/README.md"
36
+ },
37
+ {
38
+ "source": "files/.github/workflows/eval.yml",
39
+ "target": ".github/workflows/eval.yml"
40
+ },
41
+ {
42
+ "source": "files/scripts/eval-baseline.mjs",
43
+ "target": "scripts/eval-baseline.mjs"
44
+ }
45
+ ],
46
+ "contextHints": {
47
+ "commands": [
48
+ "node tests/eval/run-eval.mjs",
49
+ "node scripts/eval-baseline.mjs --write"
50
+ ],
51
+ "extensionPoints": [
52
+ "tests/eval/scenarios.json",
53
+ "tests/eval/judge.mjs"
54
+ ]
55
+ },
56
+ "keywords": [
57
+ "agent eval",
58
+ "agent evaluation",
59
+ "eval harness",
60
+ "llm judge",
61
+ "llm as judge",
62
+ "quality gate",
63
+ "agent quality",
64
+ "scoring pipeline",
65
+ "benchmark agent",
66
+ "eval integrated",
67
+ "measurable agent"
68
+ ],
69
+ "tieredKeywords": {
70
+ "tier1": ["agent eval", "eval harness", "quality gate"],
71
+ "tier2": ["llm judge", "scoring pipeline", "benchmark agent", "eval integrated"]
72
+ }
73
+ }
@@ -13,4 +13,18 @@ remappings = [
13
13
  "forge-std/=lib/forge-std/src/"
14
14
  ]
15
15
 
16
+ # forge lint — tuned so agent-written DEX/swap/vault scaffolds survive the
17
+ # VB "lint" layer without silencing real bug classes. `info` severity is
18
+ # naming-convention noise (`mixed-case-variable`,
19
+ # `screaming-snake-case-immutable`) that every real-world swap/vault
20
+ # contract trips on. `test/**` and `script/**` are fixture code where
21
+ # bytes32 casts + raw arithmetic are routine and not real bugs.
22
+ #
23
+ # History: pre-R1 this config was absent → forge lint ran default strict
24
+ # → 14/14 dex-swap/ethereum-l1 buildouts failed on unsafe-typecast in
25
+ # test files. Closing the cluster that dominated buildout_pass_rate.
26
+ [lint]
27
+ severity = ["high", "med", "gas"]
28
+ ignore = ["test/**/*", "script/**/*"]
29
+
16
30
  # See more at https://book.getfoundry.sh/reference/config/overview
@@ -15,3 +15,11 @@ remappings_version = false
15
15
 
16
16
  [dependencies]
17
17
  tnt-core = "0.10.6"
18
+
19
+ # forge lint — match forge-foundation's ruleset so blueprint scaffolds
20
+ # don't trip the VB "lint" layer on info-severity naming conventions.
21
+ # See registry/layers/framework/forge-foundation/files/foundry.toml for
22
+ # the shipped-incident rationale (R1: 14/14 dex-swap failures).
23
+ [lint]
24
+ severity = ["high", "med", "gas"]
25
+ ignore = ["contracts/test/**/*", "contracts/script/**/*"]