@xingular/harness 2.1.2 → 3.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 (132) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/NOTICE.md +22 -0
  3. package/README.md +67 -36
  4. package/dist/cli.js +6023 -6621
  5. package/dist/terminal-pi.js +19 -24
  6. package/docs/terminal-tui.md +27 -7
  7. package/package.json +2 -2
  8. package/src/accounts/accounts.ts +4 -3
  9. package/src/capabilities/api.ts +1 -1
  10. package/src/cli.ts +7 -3
  11. package/src/core/caller.ts +34 -0
  12. package/src/core/fs.ts +17 -1
  13. package/src/harness.ts +55 -66
  14. package/src/{memory → history}/recall.test.ts +1 -1
  15. package/src/{memory → history}/recall.ts +1 -1
  16. package/src/{memory → history}/trajectory.test.ts +4 -4
  17. package/src/{memory → history}/trajectory.ts +1 -1
  18. package/src/hub/hub.test.ts +4 -5
  19. package/src/hub/hub.ts +2 -2
  20. package/src/index.ts +28 -14
  21. package/src/mcp/connections.ts +1 -1
  22. package/src/mcp/service.ts +1 -1
  23. package/src/pi/proxy.test.ts +68 -0
  24. package/src/pi/runtime.test.ts +61 -0
  25. package/src/pi/runtime.ts +50 -0
  26. package/src/plugins/api.ts +11 -9
  27. package/src/plugins/builtin.ts +22 -40
  28. package/src/plugins/catalogue.test.ts +13 -41
  29. package/src/plugins/catalogue.ts +3 -32
  30. package/src/plugins/discover.ts +2 -1
  31. package/src/plugins/firstParty.test.ts +1 -1
  32. package/src/plugins/hooks.test.ts +21 -71
  33. package/src/plugins/hooks.ts +6 -7
  34. package/src/plugins/identity.ts +16 -2
  35. package/src/plugins/install.test.ts +1 -1
  36. package/src/plugins/install.ts +2 -1
  37. package/src/plugins/inventory.ts +19 -18
  38. package/src/plugins/plugins.ts +38 -2
  39. package/src/prompt/compile.test.ts +82 -193
  40. package/src/prompt/compile.ts +98 -363
  41. package/src/refine/cli.ts +114 -0
  42. package/src/refine/demo.ts +124 -0
  43. package/src/refine/evidence.ts +349 -0
  44. package/src/refine/host.ts +102 -0
  45. package/src/refine/ledger.ts +104 -0
  46. package/src/refine/library.ts +192 -0
  47. package/src/refine/pi.test.ts +90 -0
  48. package/src/refine/pi.ts +218 -0
  49. package/src/refine/prompts.ts +430 -0
  50. package/src/refine/recursion.ts +353 -0
  51. package/src/refine/refine.test.ts +140 -0
  52. package/src/refine/runner.ts +205 -0
  53. package/src/refine/selection.ts +291 -0
  54. package/src/refine/service.ts +194 -0
  55. package/src/refine/trajectory.ts +71 -0
  56. package/src/refine/types.ts +91 -0
  57. package/src/server/methods.ts +137 -123
  58. package/src/server/server.test.ts +37 -30
  59. package/src/sessions/contracts.ts +6 -1
  60. package/src/sessions/conversation.test.ts +2 -27
  61. package/src/sessions/policy.ts +1 -1
  62. package/src/sessions/pricing.ts +3 -1
  63. package/src/sessions/registry.ts +5 -1
  64. package/src/sessions/runStore.ts +3 -1
  65. package/src/sessions/runner.ts +10 -5
  66. package/src/sessions/sessions.test.ts +45 -6
  67. package/src/sessions/sessions.ts +40 -17
  68. package/src/sessions/subagentService.test.ts +22 -1
  69. package/src/sessions/subagentService.ts +14 -1
  70. package/src/sessions/subagents.ts +2 -0
  71. package/src/terminal/cli.ts +24 -48
  72. package/src/terminal/pluginControl.test.ts +7 -8
  73. package/src/terminal/socket.ts +3 -3
  74. package/src/terminal/terminal.test.ts +21 -39
  75. package/src/terminal/terminals.ts +20 -13
  76. package/src/terminal/tui/application.test.ts +58 -0
  77. package/src/terminal/tui/application.ts +10 -4
  78. package/src/terminal/tui/clipboardFiles.test.ts +94 -0
  79. package/src/terminal/tui/clipboardFiles.ts +86 -0
  80. package/src/terminal/tui/piAdapter.ts +7 -4
  81. package/src/terminal/tui/runtime.ts +2 -0
  82. package/src/tools/definitions.ts +49 -132
  83. package/src/tools/dispatch.ts +78 -0
  84. package/src/tools/harnessTools.ts +23 -0
  85. package/src/tools/spawnSubagent.ts +3 -3
  86. package/src/tools/types.ts +1 -1
  87. package/src/tree/components.ts +221 -0
  88. package/src/tree/materialise.ts +122 -0
  89. package/src/tree/seed.ts +123 -0
  90. package/src/tree/store.ts +193 -0
  91. package/src/tree/surface.ts +135 -0
  92. package/src/tree/tree.test.ts +126 -0
  93. package/src/tree/tree.ts +346 -0
  94. package/src/tree/trust.ts +86 -0
  95. package/src/wiki/notes.ts +313 -0
  96. package/src/memory/api.test.ts +0 -82
  97. package/src/memory/api.ts +0 -303
  98. package/src/memory/blocks.test.ts +0 -78
  99. package/src/memory/blocks.ts +0 -408
  100. package/src/memory/caller.ts +0 -69
  101. package/src/memory/curation.test.ts +0 -206
  102. package/src/memory/curation.ts +0 -64
  103. package/src/memory/dispatch.ts +0 -64
  104. package/src/memory/facts.test.ts +0 -41
  105. package/src/memory/facts.ts +0 -248
  106. package/src/memory/graph.ts +0 -103
  107. package/src/memory/health.test.ts +0 -130
  108. package/src/memory/health.ts +0 -74
  109. package/src/memory/maintainer.ts +0 -272
  110. package/src/memory/maintenance.test.ts +0 -192
  111. package/src/memory/maintenance.ts +0 -679
  112. package/src/memory/migrate.ts +0 -79
  113. package/src/memory/operations.ts +0 -150
  114. package/src/memory/proposals.test.ts +0 -168
  115. package/src/memory/proposals.ts +0 -584
  116. package/src/memory/references.ts +0 -77
  117. package/src/memory/runContext.ts +0 -93
  118. package/src/memory/store.test.ts +0 -66
  119. package/src/memory/store.ts +0 -385
  120. package/src/memory/wiki.test.ts +0 -47
  121. package/src/memory/wiki.ts +0 -423
  122. package/src/memory/wikiIndex.test.ts +0 -88
  123. package/src/memory/wikiIndex.ts +0 -117
  124. package/src/plugins/proposal.ts +0 -121
  125. package/src/skills/registry.test.ts +0 -104
  126. package/src/skills/registry.ts +0 -413
  127. package/src/tools/maintenance.ts +0 -29
  128. package/src/tools/memoryTools.ts +0 -22
  129. package/src/tools/pluginPropose.ts +0 -41
  130. package/src/tools/registryWrite.test.ts +0 -101
  131. package/src/tools/registryWrite.ts +0 -182
  132. /package/src/{memory → core}/secrets.ts +0 -0
@@ -0,0 +1,114 @@
1
+ // `xingular-harness refine`: refinement from a terminal.
2
+ //
3
+ // refine demo [--k N] [--mode parallel|sequential] [--out <dir>]
4
+ // Runs the deterministic ledger demo end to end with no model and no credentials.
5
+ // refine run --tree <dir> --task <file.md> [--source <dir>] [--scope <id>] [--k N]
6
+ // [--mode parallel|sequential] [--data <dir>] [--state <dir>] [--model <id>]
7
+ // Refines a harness tree on one task with the default Pi account.
8
+ import { mkdtempSync, readFileSync } from 'node:fs'
9
+ import { homedir, tmpdir } from 'node:os'
10
+ import { basename, join, resolve } from 'node:path'
11
+
12
+ import { createHarness } from '../harness'
13
+ import * as store from '../tree/store'
14
+ import { PI_SURFACE } from '../tree/surface'
15
+ import { readTree } from '../tree/tree'
16
+ import { DemoExecutor, DemoImprover, demoBaseTree, demoTasks } from './demo'
17
+ import { Library } from './library'
18
+ import { runRefinement, type RefineSummary } from './runner'
19
+ import type { RecursionMode } from './types'
20
+
21
+ export const REFINE_USAGE = `Usage: xingular-harness refine <demo | run> [options]
22
+
23
+ demo [--k N] [--mode parallel|sequential] [--out <dir>]
24
+ Run the deterministic demo: no model, no credentials.
25
+ run --tree <dir> --task <file.md> [--source <dir>] [--scope <id>] [--k N]
26
+ [--mode parallel|sequential] [--data <dir>] [--state <dir>] [--model <id>]
27
+ Refine a harness on one task with the default account. Costs about
28
+ 4k executions and 3k improver sessions.
29
+ `
30
+
31
+ interface Parsed {
32
+ command: string | undefined
33
+ values: Record<string, string>
34
+ }
35
+
36
+ function parse(argv: string[]): Parsed {
37
+ const [command, ...rest] = argv
38
+ const values: Record<string, string> = {}
39
+ for (let i = 0; i < rest.length; i++) {
40
+ const arg = rest[i]!
41
+ if (arg.startsWith('--') && i + 1 < rest.length) values[arg.slice(2)] = rest[++i]!
42
+ }
43
+ return { command, values }
44
+ }
45
+
46
+ function mode(value: string | undefined): RecursionMode {
47
+ if (!value || value === 'parallel') return 'parallel'
48
+ if (value === 'sequential') return 'sequential'
49
+ throw new Error('--mode is parallel or sequential.')
50
+ }
51
+
52
+ function describe(summary: RefineSummary, write: (text: string) => void): void {
53
+ const ledger = summary.recursion?.ledger
54
+ write(`${summary.taskId}: ${summary.status}${ledger ? ` · R=${ledger.R}/${ledger.expectedR} executions, P=${ledger.P}/${ledger.expectedP} improver sessions` : ''} · $${summary.usd.toFixed(4)}\n`)
55
+ if (summary.selection?.chosenLabel) write(` chose ${summary.selection.chosenLabel}${summary.chosenIsBase ? ' (unchanged harness)' : ''}: ${summary.selection.why.split('\n')[0]}\n`)
56
+ if (summary.experience?.lesson) write(` lesson: ${summary.experience.lesson}\n`)
57
+ for (const error of summary.errors) write(` ! ${error}\n`)
58
+ }
59
+
60
+ export async function runRefineCli(argv: string[], io: { write: (text: string) => void } = { write: (text) => void process.stdout.write(text) }): Promise<number> {
61
+ const { command, values } = parse(argv)
62
+ try {
63
+ if (command === 'demo') {
64
+ const root = values.out ? resolve(values.out) : mkdtempSync(join(tmpdir(), 'harness-refine-demo-'))
65
+ const k = Number(values.k ?? 1)
66
+ store.seed(root, 'demo', demoBaseTree(), 'demo base harness')
67
+ const executor = new DemoExecutor(root)
68
+ const improver = new DemoImprover()
69
+ for (const task of demoTasks(root)) {
70
+ const summary = await runRefinement({ root, id: `${task.id}-${mode(values.mode)}-${k}`, task, k, mode: mode(values.mode), executor, improver })
71
+ describe(summary, io.write)
72
+ }
73
+ io.write(`library: ${new Library(root, 'demo', 'accounting').entries().join(', ') || '(empty)'}\nrecords: ${join(root, 'refine')}\n`)
74
+ return 0
75
+ }
76
+ if (command === 'run') {
77
+ if (!values.tree || !values.task) {
78
+ io.write(REFINE_USAGE)
79
+ return 2
80
+ }
81
+ const base = join(homedir(), '.xingular-harness')
82
+ const harness = createHarness({ dataRoot: resolve(values.data ?? join(base, 'data')), stateDir: resolve(values.state ?? join(base, 'state')) })
83
+ try {
84
+ const scope = values.scope ?? basename(resolve(values.tree)).replace(/[^A-Za-z0-9_-]/g, '-')
85
+ const tree = readTree(PI_SURFACE, resolve(values.tree))
86
+ store.commit(harness.env.root, scope, tree, { kind: 'import', note: `from ${values.tree}` })
87
+ const prompt = readFileSync(resolve(values.task), 'utf8')
88
+ const { id } = harness.refine.start({
89
+ task: { id: basename(values.task).replace(/\.md$/, '').replace(/[^A-Za-z0-9_-]/g, '-'), prompt, scope, sourceDir: values.source ? resolve(values.source) : null },
90
+ k: Number(values.k ?? 1),
91
+ mode: mode(values.mode),
92
+ improver: values.model ? { model: values.model } : {}
93
+ })
94
+ harness.bus.on((notice) => {
95
+ if (notice.type === 'refine.event' && notice.id === id) {
96
+ const event = notice.event as { type: string; label?: string; phase?: string }
97
+ if (event.type === 'phase') io.write(`· ${event.phase}\n`)
98
+ else if (event.type === 'stage.finished' || event.type === 'rollout.finished') io.write(` ${event.type === 'stage.finished' ? 'proposed' : 'ran'} ${event.label}\n`)
99
+ }
100
+ })
101
+ const summary = await harness.refine.wait(id)
102
+ if (summary) describe(summary, io.write)
103
+ return summary?.status === 'ok' ? 0 : 1
104
+ } finally {
105
+ await harness.close()
106
+ }
107
+ }
108
+ io.write(REFINE_USAGE)
109
+ return command ? 2 : 0
110
+ } catch (error) {
111
+ io.write(`${error instanceof Error ? error.message : String(error)}\n`)
112
+ return 1
113
+ }
114
+ }
@@ -0,0 +1,124 @@
1
+ // A deterministic local executor and improver that exercise the real refinement workflow with
2
+ // no model and no credentials: the base harness sums only positive ledger amounts, and the
3
+ // scripted improver removes that filter.
4
+ //
5
+ // Ported from Harness² (Apache-2.0, Google LLC; `harness2/demo.py`).
6
+ import { spawnSync } from 'node:child_process'
7
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+
11
+ import type { Tree } from '../tree/surface'
12
+ import type { Executor, Improver, ImproverCall, ImproverReply, RefineTask, Rollout } from './types'
13
+
14
+ export const DEMO_SCRIPT = `import { readFileSync } from 'node:fs'
15
+
16
+ const rows = readFileSync(process.argv[2], 'utf8').trim().split('\\n').slice(1)
17
+ const cents = rows.map((row) => Math.round(Number(row) * 100))
18
+ const total = cents.filter((amount) => amount > 0).reduce((sum, amount) => sum + amount, 0)
19
+ console.log(\`Total: \${(total / 100).toFixed(2)}\`)
20
+ `
21
+
22
+ export function demoBaseTree(): Tree {
23
+ return {
24
+ 'systemprompt.md': 'Reconcile the ledger using node scripts/summarize.mjs.\n',
25
+ 'guardrails.md': '',
26
+ 'memory.md': '',
27
+ 'tool_guidance.md': '',
28
+ 'scripts/summarize.mjs': DEMO_SCRIPT
29
+ }
30
+ }
31
+
32
+ export const DEMO_LEDGERS: Record<string, string> = {
33
+ 'ledger-1': '100.25\n-20.10\n5.00\n',
34
+ 'ledger-2': '35.50\n-10.25\n-5.00\n'
35
+ }
36
+
37
+ /** Writes the demo's source ledgers under `root` and returns the tasks. */
38
+ export function demoTasks(root: string, scope = 'demo'): RefineTask[] {
39
+ return Object.entries(DEMO_LEDGERS).map(([id, amounts]) => {
40
+ const sourceDir = join(root, 'sources', id)
41
+ mkdirSync(sourceDir, { recursive: true })
42
+ writeFileSync(join(sourceDir, 'ledger.csv'), `amount\n${amounts}`)
43
+ writeFileSync(join(sourceDir, 'reference_answer.txt'), 'never shown to a model')
44
+ return {
45
+ id,
46
+ scope,
47
+ domain: 'accounting',
48
+ prompt: 'Sum every signed amount in ledger.csv. Return Total: with two decimal places.',
49
+ sourceDir,
50
+ leakGlobs: ['reference_answer.txt'],
51
+ prompts: { work: 'reconcile signed amounts in a transaction ledger' }
52
+ }
53
+ })
54
+ }
55
+
56
+ export class DemoExecutor implements Executor {
57
+ executions = 0
58
+
59
+ constructor(private readonly root: string) {}
60
+
61
+ async execute(task: RefineTask, harnessDir: string | null, label: string, runId: string): Promise<Rollout> {
62
+ this.executions++
63
+ const dir = join(this.root, 'executions', runId)
64
+ mkdirSync(dir, { recursive: true })
65
+ const script = join(harnessDir ?? '', 'scripts', 'summarize.mjs')
66
+ const result = spawnSync(process.execPath, [script, join(task.sourceDir ?? '', 'ledger.csv')], { encoding: 'utf8', timeout: 10_000 })
67
+ if (result.status !== 0) {
68
+ return { label, runId, dir, ok: false, delivered: false, costUsd: 0, wallMs: 0, error: result.stderr || 'script failed' }
69
+ }
70
+ writeFileSync(join(dir, 'answer.txt'), result.stdout)
71
+ writeFileSync(join(dir, 'trajectory.md'), `[ACTION] Run scripts/summarize.mjs on ledger.csv\n[OBSERVATION]\n${result.stdout}`)
72
+ return { label, runId, dir, ok: true, delivered: true, costUsd: 0, wallMs: 0, error: '' }
73
+ }
74
+
75
+ trajectory(rollout: Rollout): string {
76
+ return readFileSync(join(rollout.dir, 'trajectory.md'), 'utf8')
77
+ }
78
+
79
+ deliverableFiles(rollout: Rollout): Record<string, string> {
80
+ const path = join(rollout.dir, 'answer.txt')
81
+ return existsSync(path) ? { 'answer.txt': readFileSync(path, 'utf8') } : {}
82
+ }
83
+
84
+ validateTree(_task: RefineTask, tree: Tree): void {
85
+ if (!tree['scripts/summarize.mjs']) throw new Error('The local executor requires scripts/summarize.mjs')
86
+ }
87
+ }
88
+
89
+ function signedTotal(ledger: string): string {
90
+ const cents = ledger.trim().split('\n').slice(1).map((row) => Math.round(Number(row) * 100))
91
+ return (cents.reduce((sum, amount) => sum + amount, 0) / 100).toFixed(2)
92
+ }
93
+
94
+ /** Scripted edits, selection and reflection. */
95
+ export class DemoImprover implements Improver {
96
+ readonly calls: string[] = []
97
+
98
+ async call(input: ImproverCall): Promise<ImproverReply> {
99
+ this.calls.push(input.label)
100
+ const context = input.contextDir
101
+ let workspace: string | null = null
102
+ let text: string
103
+ if (input.label.startsWith('select.')) {
104
+ const expected = `Total: ${signedTotal(readFileSync(join(context, 'source', 'ledger.csv'), 'utf8'))}`
105
+ const ids = readdirSync(join(context, 'candidates')).filter((name) => name.endsWith('.md')).sort()
106
+ const chosen = ids.find((name) => readFileSync(join(context, 'candidates', name), 'utf8').includes(expected))?.replace(/\.md$/, '') ?? ids[0]!.replace(/\.md$/, '')
107
+ text = `<why>The signed total matches the source ledger.</why>\n\`\`\`choice\n${chosen}\n\`\`\``
108
+ } else if (input.label.startsWith('experience.')) {
109
+ const changed = readFileSync(join(context, 'harness', 'scripts', 'summarize.mjs'), 'utf8') !== readFileSync(join(context, 'base_harness', 'scripts', 'summarize.mjs'), 'utf8')
110
+ text =
111
+ `<edits>\ncomponent: scripts/summarize.mjs\ncandidates: inv1\nchange: dropped the positive-only filter from the amount sum\nobserved: the probe total included the credits the base run omitted\nverdict: ${changed ? 'helped' : 'no observed effect'}\n</edits>\n\n` +
112
+ (changed
113
+ ? '<experience>\ncue: When a ledger includes credits and debits\nlesson: Include both signs when summing in scripts/summarize.mjs.\nevidence: The scripts/summarize.mjs row -- removing the positive-only filter changed the probe total to include the credits omitted by the base trajectory.\nconfidence: high for this observed arithmetic contrast\n</experience>'
114
+ : 'NO_ENTRY')
115
+ } else {
116
+ workspace = mkdtempSync(join(tmpdir(), 'harness-demo-'))
117
+ cpSync(join(context, 'harness'), workspace, { recursive: true })
118
+ const script = join(workspace, 'scripts', 'summarize.mjs')
119
+ writeFileSync(script, readFileSync(script, 'utf8').replace('.filter((amount) => amount > 0)', ''))
120
+ text = '<notes>Keep signed amounts: the base total omitted credits in the source ledger.</notes>'
121
+ }
122
+ return { label: input.label, text, costUsd: 0, attempts: 1, cached: false, workspace }
123
+ }
124
+ }
@@ -0,0 +1,349 @@
1
+ // The evidence folder an improver reads. Evidence is served as files and the prompt is a short
2
+ // pointer, so nothing is truncated to fit a prompt:
3
+ //
4
+ // context/task.md the task as the agent received it
5
+ // context/source/ the material it was done from
6
+ // context/rollouts/<label>.md full trajectories
7
+ // context/deliverables/<label>/ what each rollout produced
8
+ // context/harness/<rel> the tree being edited
9
+ // context/library/INDEX.md earlier tasks in this area and their full harnesses
10
+ // context/pool/<label>/ candidate harnesses, their diffs and rollouts
11
+ // context/decisions/<label>.md earlier edit and composition notes
12
+ //
13
+ // Ported from Harness² (Apache-2.0, Google LLC; `harness2/core/context.py`).
14
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
15
+ import { dirname, extname, join, relative, sep } from 'node:path'
16
+
17
+ import { PI_SURFACE, type Tree } from '../tree/surface'
18
+ import { readTree } from '../tree/tree'
19
+ import type { Executor, RefineTask, Rollout } from './types'
20
+
21
+ /** Strings that only a grader's own output contains. A tripwire, not the defence itself. */
22
+ export const GRADER_VERDICT_MARKERS = [
23
+ 'deterministic_eval_summary',
24
+ 'deterministic_eval_result',
25
+ 'judge_scope_and_deterministic_status',
26
+ 'Judge findings on the rendered result',
27
+ '"passed_count"',
28
+ '"pass_rate"',
29
+ '"test_pass_rate"',
30
+ 'review_verdict',
31
+ '"reviewVerdict"'
32
+ ] as const
33
+
34
+ const SCANNED = new Set(['.md', '.txt', '.json', '.csv', '.tsv', '.yaml', '.yml', '.html', '.htm', '.log', '.py', '.js', '.mjs', '.ts', '.sh', '.xml', '.ini', '.toml', '.patch', '.diff', ''])
35
+
36
+ /** Minimum per-observation truncation; lower values fabricate defects. */
37
+ export const MIN_OBS_TRUNC = 10_000
38
+
39
+ function files(dir: string): string[] {
40
+ const out: string[] = []
41
+ let names: string[]
42
+ try {
43
+ names = readdirSync(dir)
44
+ } catch {
45
+ return out
46
+ }
47
+ for (const name of names.sort()) {
48
+ const path = join(dir, name)
49
+ let isDir = false
50
+ try {
51
+ isDir = statSync(path).isDirectory()
52
+ } catch {
53
+ continue
54
+ }
55
+ if (isDir) out.push(...files(path))
56
+ else out.push(path)
57
+ }
58
+ return out
59
+ }
60
+
61
+ function rel(root: string, path: string): string {
62
+ return relative(root, path).split(sep).join('/')
63
+ }
64
+
65
+ function globToRegExp(glob: string): RegExp {
66
+ const pattern = glob.replace(/\\/g, '/').replace(/^\.\//, '')
67
+ let out = ''
68
+ for (const ch of pattern) {
69
+ if (ch === '*') out += '[^]*'
70
+ else if (ch === '?') out += '.'
71
+ else out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&')
72
+ }
73
+ return new RegExp(`^(?:[^]*/)?${out}$`)
74
+ }
75
+
76
+ /** Refuses a folder holding a path the task marks as an answer key. */
77
+ export function assertNoLeakPaths(root: string, globs: readonly string[] = []): void {
78
+ if (!globs.length || !existsSync(root)) return
79
+ const rx = globs.map(globToRegExp)
80
+ const hits = files(root).map((path) => rel(root, path)).filter((path) => rx.some((r) => r.test(path)))
81
+ if (hits.length) throw new Error(`answer-key path(s) would enter model context: ${hits.sort().slice(0, 8).join(', ')}`)
82
+ }
83
+
84
+ /** Refuses a folder containing a grader's output, by file name and by content. */
85
+ export function assertNoGraderVerdicts(root: string, markers: readonly string[] = GRADER_VERDICT_MARKERS): void {
86
+ if (!existsSync(root)) return
87
+ for (const path of files(root)) {
88
+ const name = rel(root, path)
89
+ for (const marker of markers) {
90
+ if (name.includes(marker)) throw new Error(`grader verdict would enter model context: '${name}' matched marker '${marker}' in its NAME. Find the read path that produced it.`)
91
+ }
92
+ if (!SCANNED.has(extname(path).toLowerCase())) continue
93
+ let body: string
94
+ try {
95
+ body = readFileSync(path, 'utf8')
96
+ } catch {
97
+ continue
98
+ }
99
+ for (const marker of markers) {
100
+ const at = body.indexOf(marker)
101
+ if (at >= 0) throw new Error(`grader verdict would enter model context: '${name}' contains marker '${marker}' at offset ${at}. Find the read path that produced it.`)
102
+ }
103
+ }
104
+ }
105
+
106
+ /** The same tripwire on a tree, before it is rolled or stored. */
107
+ export function assertTreeHasNoGraderVerdicts(tree: Record<string, string>, markers: readonly string[] = GRADER_VERDICT_MARKERS): void {
108
+ for (const [name, body] of Object.entries(tree).sort()) {
109
+ for (const marker of markers) {
110
+ if (name.includes(marker)) throw new Error(`harness file '${name}' has grader-verdict marker '${marker}' in its NAME; rename it`)
111
+ if (SCANNED.has(extname(name).toLowerCase()) && body.includes(marker)) throw new Error(`harness file '${name}' contains grader-verdict marker '${marker}'; remove that string -- files carrying it cannot be served as evidence to later tasks`)
112
+ }
113
+ }
114
+ }
115
+
116
+ function copyFolder(src: string, dst: string): number {
117
+ let n = 0
118
+ for (const path of files(src)) {
119
+ if (path.includes(`${sep}__pycache__${sep}`) || path.endsWith('.pyc')) continue
120
+ const target = join(dst, relative(src, path))
121
+ mkdirSync(dirname(target), { recursive: true })
122
+ copyFileSync(path, target)
123
+ n++
124
+ }
125
+ return n
126
+ }
127
+
128
+ /** Copies the task's source material under `root/source`, minus answer-key paths. */
129
+ export function copySource(task: RefineTask, root: string): number {
130
+ const src = task.sourceDir
131
+ if (!src || !existsSync(src)) return 0
132
+ const leak = (task.leakGlobs ?? []).map(globToRegExp)
133
+ let n = 0
134
+ for (const path of files(src)) {
135
+ const name = rel(src, path)
136
+ if (name.split('/').includes('__pycache__') || leak.some((r) => r.test(name))) continue
137
+ const target = join(root, 'source', name)
138
+ mkdirSync(dirname(target), { recursive: true })
139
+ copyFileSync(path, target)
140
+ n++
141
+ }
142
+ if (n) assertNoLeakPaths(join(root, 'source'), task.leakGlobs)
143
+ return n
144
+ }
145
+
146
+ /** A line-based unified diff, enough for a reader to see what changed. */
147
+ export function unifiedDiff(before: Tree, after: Tree, fromLabel: string, toLabel: string): string {
148
+ const out: string[] = []
149
+ for (const name of [...new Set([...Object.keys(before), ...Object.keys(after)])].sort()) {
150
+ const a = before[name] ?? ''
151
+ const b = after[name] ?? ''
152
+ if (a === b) continue
153
+ out.push(`--- ${fromLabel}/${name}`, `+++ ${toLabel}/${name}`)
154
+ const x = a ? a.split('\n') : []
155
+ const y = b ? b.split('\n') : []
156
+ // A longest-common-subsequence walk; trees are small enough for the quadratic table.
157
+ const dp: number[][] = Array.from({ length: x.length + 1 }, () => new Array<number>(y.length + 1).fill(0))
158
+ for (let i = x.length - 1; i >= 0; i--) for (let j = y.length - 1; j >= 0; j--) dp[i]![j] = x[i] === y[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!)
159
+ let i = 0
160
+ let j = 0
161
+ while (i < x.length || j < y.length) {
162
+ if (i < x.length && j < y.length && x[i] === y[j]) {
163
+ out.push(` ${x[i]}`)
164
+ i++
165
+ j++
166
+ } else if (j < y.length && (i >= x.length || dp[i]![j + 1]! >= dp[i + 1]![j]!)) {
167
+ out.push(`+${y[j]}`)
168
+ j++
169
+ } else {
170
+ out.push(`-${x[i]}`)
171
+ i++
172
+ }
173
+ }
174
+ }
175
+ return out.length ? `${out.join('\n')}\n` : ''
176
+ }
177
+
178
+ export interface PoolEntry {
179
+ label: string
180
+ /** The candidate's harness folder, or null. */
181
+ harnessDir: string | null
182
+ rollout: Rollout | null
183
+ }
184
+
185
+ export interface EvidenceTree {
186
+ root: string
187
+ taskId: string
188
+ rollouts: string[]
189
+ deliverables: string[]
190
+ harnessFiles: number
191
+ library: boolean
192
+ pool: string[]
193
+ failed: string[]
194
+ sourceFiles: number
195
+ decisions: number
196
+ }
197
+
198
+ /** The evidence-layout listing that goes in a mission; it mentions only what exists. */
199
+ export function layout(tree: EvidenceTree): string {
200
+ const lines = ['context/task.md the task as the agent received it']
201
+ if (tree.sourceFiles) lines.push('context/source/ task source materials')
202
+ if (tree.decisions) lines.push('context/decisions/ earlier edits and composition decisions')
203
+ if (tree.rollouts.length) {
204
+ const names = tree.rollouts.slice(0, 6).map((label) => `${label}.md`).join(', ')
205
+ const more = tree.rollouts.length > 6 ? ` (+${tree.rollouts.length - 6} more)` : ''
206
+ lines.push(`context/rollouts/ FULL trajectories: ${names}${more}`)
207
+ }
208
+ if (tree.deliverables.length) lines.push('context/deliverables/<label>/ the files each rollout produced, one per artifact (browse INDEX.md for the set)')
209
+ if (tree.harnessFiles) lines.push(`context/harness/ the harness you are editing (${tree.harnessFiles} files)`)
210
+ if (tree.pool.length) lines.push(`context/pool/ ${tree.pool.length} candidates, each with its harness and its rollout`)
211
+ if (tree.library) lines.push('context/library/ earlier tasks in this area -- browse INDEX.md first, then open what looks relevant')
212
+ return lines.join('\n')
213
+ }
214
+
215
+ export interface BuildInput {
216
+ task: RefineTask
217
+ executor: Executor
218
+ rollouts?: readonly Rollout[]
219
+ harnessDir?: string | null
220
+ libraryDir?: string | null
221
+ pool?: readonly PoolEntry[]
222
+ decisions?: Record<string, string>
223
+ obsTrunc?: number
224
+ }
225
+
226
+ function writeRollout(dest: string, rollout: Rollout, executor: Executor, obsTrunc: number): string {
227
+ let failure = ''
228
+ mkdirSync(join(dest, 'rollouts'), { recursive: true })
229
+ try {
230
+ writeFileSync(join(dest, 'rollouts', `${rollout.label}.md`), executor.trajectory(rollout, { obsTrunc }))
231
+ } catch (error) {
232
+ const message = error instanceof Error ? error.message : String(error)
233
+ writeFileSync(join(dest, 'rollouts', `${rollout.label}.md`), `(trajectory unavailable: ${message})`)
234
+ failure = `${rollout.label}: ${message}`
235
+ }
236
+ const dir = join(dest, 'deliverables', rollout.label)
237
+ mkdirSync(dir, { recursive: true })
238
+ let produced: Record<string, string> = {}
239
+ try {
240
+ produced = executor.deliverableFiles(rollout)
241
+ } catch (error) {
242
+ produced = { 'unavailable.md': `(deliverable unavailable: ${error instanceof Error ? error.message : String(error)})` }
243
+ }
244
+ const names = Object.keys(produced).sort()
245
+ for (const name of names) {
246
+ const target = join(dir, name)
247
+ mkdirSync(dirname(target), { recursive: true })
248
+ writeFileSync(target, produced[name]!)
249
+ }
250
+ writeFileSync(join(dir, 'INDEX.md'), names.length ? `# ${rollout.label} produced ${names.length} file(s)\n\n${names.map((name) => `- \`${name}\``).join('\n')}\n` : `# ${rollout.label} produced nothing\n`)
251
+ return failure
252
+ }
253
+
254
+ function renderDeliverable(executor: Executor, rollout: Rollout): string {
255
+ const produced = executor.deliverableFiles(rollout)
256
+ const names = Object.keys(produced).sort()
257
+ if (!names.length) return ''
258
+ if (names.length === 1) return produced[names[0]!]!
259
+ return names.map((name) => `## ${name}\n\n${produced[name]}`).join('\n\n')
260
+ }
261
+
262
+ export { renderDeliverable }
263
+
264
+ /**
265
+ * Writes the evidence folder and returns what landed, so a mission describes only that. A
266
+ * trajectory is written in full; `obsTrunc` applies per observation.
267
+ */
268
+ export function build(dest: string, input: BuildInput): EvidenceTree {
269
+ const { task, executor } = input
270
+ rmSync(dest, { recursive: true, force: true })
271
+ mkdirSync(dest, { recursive: true })
272
+ const obsTrunc = Math.max(input.obsTrunc ?? 50_000, MIN_OBS_TRUNC)
273
+
274
+ writeFileSync(join(dest, 'task.md'), task.prompt)
275
+ const sourceFiles = copySource(task, dest)
276
+ for (const [label, notes] of Object.entries(input.decisions ?? {})) {
277
+ mkdirSync(join(dest, 'decisions'), { recursive: true })
278
+ writeFileSync(join(dest, 'decisions', `${label}.md`), notes || 'No decision notes returned.')
279
+ }
280
+
281
+ const rollouts: string[] = []
282
+ const deliverables: string[] = []
283
+ const failed: string[] = []
284
+ for (const rollout of input.rollouts ?? []) {
285
+ const failure = writeRollout(dest, rollout, executor, obsTrunc)
286
+ if (failure) failed.push(failure)
287
+ else rollouts.push(rollout.label)
288
+ deliverables.push(rollout.label)
289
+ }
290
+
291
+ let harnessFiles = 0
292
+ if (input.harnessDir && existsSync(input.harnessDir)) {
293
+ assertNoLeakPaths(input.harnessDir, task.leakGlobs)
294
+ harnessFiles = copyFolder(input.harnessDir, join(dest, 'harness'))
295
+ }
296
+
297
+ const pool: string[] = []
298
+ for (const entry of input.pool ?? []) {
299
+ const dir = join(dest, 'pool', entry.label)
300
+ mkdirSync(dir, { recursive: true })
301
+ if (entry.harnessDir && existsSync(entry.harnessDir)) {
302
+ assertNoLeakPaths(entry.harnessDir, task.leakGlobs)
303
+ copyFolder(entry.harnessDir, join(dir, 'harness'))
304
+ const reference = readTree(PI_SURFACE, join(dest, 'harness'))
305
+ const candidate = readTree(PI_SURFACE, entry.harnessDir)
306
+ writeFileSync(join(dir, 'harness.diff'), unifiedDiff(reference, candidate, 'current', entry.label))
307
+ }
308
+ if (entry.rollout) {
309
+ try {
310
+ writeFileSync(join(dir, 'rollout.md'), executor.trajectory(entry.rollout, { obsTrunc }))
311
+ writeFileSync(join(dir, 'produced.md'), renderDeliverable(executor, entry.rollout) || '(nothing produced)')
312
+ } catch (error) {
313
+ writeFileSync(join(dir, 'rollout.md'), `(unavailable: ${error instanceof Error ? error.message : String(error)})`)
314
+ }
315
+ }
316
+ pool.push(entry.label)
317
+ }
318
+
319
+ let library = false
320
+ if (input.libraryDir && existsSync(input.libraryDir) && readdirSync(input.libraryDir).length) {
321
+ assertNoLeakPaths(input.libraryDir, task.leakGlobs)
322
+ cpSync(input.libraryDir, join(dest, 'library'), { recursive: true })
323
+ library = true
324
+ }
325
+
326
+ assertNoLeakPaths(dest, task.leakGlobs)
327
+ assertNoGraderVerdicts(dest)
328
+ if ((input.rollouts ?? []).length && !rollouts.length) {
329
+ throw new Error(`no trajectory could be rendered for ANY rollout -- the improver would be adapting on nothing: ${failed.slice(0, 4).join('; ')}`)
330
+ }
331
+ return { root: dest, taskId: task.id, rollouts, deliverables, harnessFiles, library, pool, failed, sourceFiles, decisions: Object.keys(input.decisions ?? {}).length }
332
+ }
333
+
334
+ /** The short pointer to the evidence on disk: the job, where the evidence is, and the instruction to open it. */
335
+ export function mission(tree: EvidenceTree, job: string): string {
336
+ return [
337
+ job.trim(),
338
+ '',
339
+ '# YOUR EVIDENCE',
340
+ '',
341
+ 'It is on disk, in `context/`. Read it -- do not answer from this message alone.',
342
+ '',
343
+ layout(tree),
344
+ '',
345
+ 'Every trajectory EVENT is present: the trajectory was not clipped to fit this mission. An individual observation may contain an explicit renderer-truncation marker; that marker is ours, not behavior by the solver. Do not diagnose it as a solver failure.',
346
+ '',
347
+ 'Read `task.md` and the current harness first. Inventory every available rollout or candidate and inspect each at least once before deciding. For a candidate, compare what changed from its editing baseline, what behavior changed in its rollout, what improved, and what regressed or remained unresolved. Then deep-read the evidence relevant to any edit. Cite the specific trajectory moment behind every change; one you cannot trace to a moment is speculation.'
348
+ ].join('\n')
349
+ }