@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
@@ -3,17 +3,16 @@ import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { afterEach, expect, test } from 'vitest'
5
5
 
6
- import { dataRoot, writeFile } from '../../tests/helpers'
6
+ import { dataRoot } from '../../tests/helpers'
7
+ import type { CallerContext } from '../core/caller'
7
8
  import { createEnv, type HarnessEnv } from '../core/env'
8
9
  import { writeJson } from '../core/fs'
9
10
  import { closeIndex } from '../core/index'
10
- import * as blocks from '../memory/blocks'
11
- import type { CallerContext } from '../memory/caller'
12
- import * as references from '../memory/references'
13
- import { readRunContext } from '../memory/runContext'
14
- import { ensureProjectNamespace, wikiDir } from '../memory/store'
15
- import { reindexWiki } from '../memory/wikiIndex'
16
- import { compile, fit, launchText, load, memoryDelta, profileFor, profiles, record, REFERENCES_BUDGET, render, saveForSession, sessionFile, sessionLaunch, setProfile, type ContextPack, type ContextUpdate, type HostSection, type Launch } from './compile'
11
+ import { materialise } from '../tree/materialise'
12
+ import * as store from '../tree/store'
13
+ import type { Tree } from '../tree/surface'
14
+ import * as notes from '../wiki/notes'
15
+ import { compile, fit, load, render, sessionFile, sessionLaunch, SECTION_ORDER, type ContextUpdate, type HostSection, type Launch } from './compile'
17
16
 
18
17
  const roots: string[] = []
19
18
  afterEach(() => roots.splice(0).forEach(closeIndex))
@@ -21,33 +20,33 @@ afterEach(() => roots.splice(0).forEach(closeIndex))
21
20
  function setup(): HarnessEnv {
22
21
  const root = dataRoot()
23
22
  roots.push(root)
24
- writeJson(join(root, 'projects.json'), [
25
- { id: 'p1', name: 'Xingular Agent', rootPath: '/src/xingular-agent', autoMaintenance: true, createdAt: 't', slug: 'xingular-agent' },
26
- { id: 'p2', name: 'Site', rootPath: '/src/site', autoMaintenance: true, createdAt: 't', slug: 'site' }
27
- ])
28
- ensureProjectNamespace(root, 'xingular-agent', 'Xingular Agent')
29
- const wiki = wikiDir(root)
30
- writeFileSync(join(wiki, 'projects/xingular-agent/decisions.md'), '---\nid: decisions\ntitle: Decisions\ntype: decision\n---\n\nTauri 2 for the shell.\n')
31
- writeFileSync(join(wiki, 'facts/pnpm.md'), '---\nid: pnpm\ntitle: Use pnpm\nkind: preference\nnamespace: /\nconfidence: high\n---\n\nUse pnpm, not npm.\n')
32
- writeFileSync(join(wiki, 'facts/on-call.md'), '---\nid: on-call\ntitle: On call this week\nkind: fact\nnamespace: /\nconfidence: high\nvolatile: true\n---\n\nAstro team is on call.\n')
33
- writeFileSync(join(wiki, 'facts/site-only.md'), '---\nid: site-only\ntitle: Site uses Astro\nkind: fact\nnamespace: /projects/site\n---\n\nAstro renders the site.\n')
34
- writeFile(join(root, 'registry/rules/tests.md'), '---\nname: Run tests\n---\n\nAlways run the tests.\n')
35
- writeFile(join(root, 'registry/skills/review/SKILL.md'), '---\nname: review\ndescription: Review a change\n---\n\nRead the diff first.\n')
36
- writeJson(join(root, 'config/registry-enabled.json'), { global: ['rules/tests'], p1: ['skills/review'] })
23
+ writeJson(join(root, 'projects.json'), [{ id: 'p1', name: 'Xingular Agent', rootPath: '/src/xingular-agent', autoMaintenance: true, createdAt: 't', slug: 'xingular-agent' }])
24
+ notes.ensureLayout(root, 'Test')
25
+ notes.write(root, 'projects/xingular-agent/decisions', '---\ntitle: Decisions\n---\n\nTauri 2 for the shell. See [[library/glossary]].\n')
26
+ notes.write(root, 'library/glossary', '# Glossary\n\nA harness is the system around a model.\n')
37
27
  mkdirSync(join(root, 'runs/run-1'), { recursive: true })
38
- reindexWiki(root)
39
28
  return createEnv(root)
40
29
  }
41
30
 
42
- const session = (project: string): CallerContext => ({ runId: 'run-1', sessionId: 's1', projectId: project, actor: 'Code' })
43
- const agent = (extra: Partial<CallerContext> = {}): CallerContext => ({ agentId: 'nora', agentName: 'Nora', actor: 'Chat', ...extra })
44
- const ids = (pack: ContextPack): string[] => pack.sections.map((entry) => entry.id)
31
+ const tree: Tree = {
32
+ 'systemprompt.md': 'You draft release notes.\n',
33
+ 'guardrails.md': 'Never publish without a review.\n- When writing a changelog, load the `changelog` skill.\n',
34
+ 'memory.md': 'Releases ship on Tuesdays.\n',
35
+ 'tool_guidance.md': 'Prefer grep over reading whole files.\n',
36
+ 'skills/changelog/SKILL.md': '---\nname: changelog\ndescription: writing a changelog\n---\nGroup by type.\n',
37
+ 'sub_agents/reader/AGENT.md': '---\ndescription: reads long logs and answers briefly\n---\nRead the log and answer in three lines.\n',
38
+ 'scripts/count.py': 'print(1)\n'
39
+ }
40
+
41
+ const agent = (extra: Partial<CallerContext> = {}): CallerContext => ({ agentId: 'nora', agentName: 'Nora', projectId: 'p1', runId: 'run-1', actor: 'Chat', ...extra })
42
+ const identity: HostSection = { id: 'identity', title: 'Identity', body: 'You are Nora.', budget: 1000, order: 10 }
43
+ const work = (body: string): HostSection => ({ id: 'work', title: 'Active work', body, budget: 1000, order: 70, dynamic: true })
44
+ const hosted = (extra: Partial<Launch> = {}): Launch => ({ sections: [identity, work('You have no unresolved work.')], tools: ['read'], ...extra })
45
45
 
46
- // What an application supplies: who the model is, its role, and what it is working on.
47
- const identity: HostSection = { id: 'identity', title: 'Identity', body: 'You are "Nora", an Xingular Agent agent.', budget: 6000, order: 10 }
48
- const role: HostSection = { id: 'role', title: 'Role', body: 'You coordinate.', budget: 1500, order: 20 }
49
- const work = (body: string | null, pinned?: string): HostSection => ({ id: 'work', title: 'Active work', body, budget: 1500, pointer: '(Call list_work for the full inventory.)', order: 70, dynamic: true, pinned: pinned ? { text: pinned, pointer: '(Call list_work for the full evidence.)' } : null })
50
- const hosted = (extra: Partial<Launch> = {}): Launch => ({ sections: [identity, role, work('You have no unresolved work.\n')], ...extra })
46
+ function scoped(env: HarnessEnv) {
47
+ store.seed(env.root, 'nora', tree)
48
+ return materialise(env.root, { scope: 'nora' }, { subagents: true })
49
+ }
51
50
 
52
51
  test('cuts at a paragraph and points to the rest', () => {
53
52
  expect(fit('short', 100, '(more)')).toEqual({ body: 'short', cut: false })
@@ -56,126 +55,46 @@ test('cuts at a paragraph and points to the rest', () => {
56
55
  expect(cut.body).toBe(`${'a'.repeat(40)}\n\n${'b'.repeat(40)}\n\n(more)`)
57
56
  })
58
57
 
59
- test('selected content is pinned and retained with automatic context off', () => {
60
- const env = setup()
61
- setProfile(env.root, 'nora', 'off')
62
- const revision = references.pin(env.root, 'skill', 'skills/review')
63
- writeFileSync(join(env.root, 'registry/skills/review/SKILL.md'), 'Changed instructions.')
64
- const newer = references.pin(env.root, 'skill', 'skills/review')
65
- const caller = agent({ runId: 'run-1' })
66
- const pack = compile(env, caller, hosted({ references: [{ kind: 'skill', id: 'skills/review', label: 'Review', revision }] }))
67
- expect(ids(pack)).toEqual(['identity', 'role', 'references'])
68
- const selected = pack.sections.find((entry) => entry.id === 'references')!
69
- expect(selected.body).toContain('Read the diff first.')
70
- expect(selected.body).not.toContain('Changed instructions.')
71
- expect(revision).not.toBe(newer)
72
- record(env.root, caller, pack)
73
- expect(references.readPinned(env.root, 'run-1', 'skills/review', revision, 0)).toContain('Read the diff first.')
74
- expect(() => references.readPinned(env.root, 'run-1', 'skills/review', newer, 0)).toThrow()
75
- expect(readRunContext(env.root, 'run-1')[0].revisionId).toBe(revision)
76
- })
77
-
78
- test('application lines sit with the references and large sources keep retrieval pointers', () => {
58
+ test('the harness tree supplies the instructions, guardrails, memory, subagents, scripts and tool guidance, in order', () => {
79
59
  const env = setup()
80
- writeFileSync(join(env.root, 'registry/skills/review/SKILL.md'), 'Review instructions.\n\n'.repeat(2000))
81
- const revision = references.pin(env.root, 'skill', 'skills/review')
82
- const pack = compile(env, agent(), { references: [{ kind: 'skill', id: 'skills/review', label: 'Review', revision }, { kind: 'line', id: 'exact-agent-id', label: 'Duplicate name', text: '- Agent "Duplicate name": exact ID "exact-agent-id". Agent messaging is unavailable in this reply.' }] })
83
- const selected = pack.sections.find((entry) => entry.id === 'references')!
84
- expect(selected.body).toContain('Agent messaging is unavailable')
85
- expect(selected.cut).toBe(true)
86
- expect(selected.size).toBeLessThanOrEqual(REFERENCES_BUDGET)
87
- expect(selected.body).toContain('wiki_read(id="skills/review", revision=')
88
- expect(selected.sources[0].revision).toBe(revision)
89
- })
90
-
91
- test('a session gets blocks, rules, skills, knowledge and tools', () => {
92
- const env = setup()
93
- const pack = compile(env, session('p1'), { connections: 'Use get_capabilities.' })
94
- expect(pack.profile).toBe('session')
95
- expect(ids(pack)).toEqual(['blocks', 'rules', 'skills', 'knowledge', 'tools'])
60
+ const pack = compile(env, agent(), hosted(), scoped(env))
61
+ const ids = pack.sections.map((entry) => entry.id)
62
+ expect(ids).toEqual(['identity', 'system', 'guardrails', 'memory', 'subagents', 'scripts', 'work', 'wiki', 'tools'])
96
63
  const text = render(env, pack)
97
- for (const expected of ['<block label="user"', '<block label="project/xingular-agent"', 'Always run the tests.', 'review (skills/review)', '/projects/xingular-agent/decisions — Decisions', 'Use pnpm', 'App tools and connected apps: Use get_capabilities.']) expect(text).toContain(expected)
98
- expect(text).not.toContain('Astro')
99
- const cli = render(env, compile(env, { ...session('p1'), surface: 'cli' }))
100
- expect(cli).toContain('xagent-wiki search')
101
- expect(cli).toContain('xagent-wiki block append')
64
+ expect(text).toContain('You draft release notes.')
65
+ expect(text).toContain('Never publish without a review.')
66
+ expect(text).toContain('Releases ship on Tuesdays.')
67
+ expect(text).toContain('- reader — reads long logs and answers briefly')
68
+ expect(text).toMatch(/scripts: count\.py/)
69
+ expect(text).toContain('### How to use your tools\n\nPrefer grep over reading whole files.')
70
+ expect(pack.harness).toEqual({ scope: 'nora', fingerprint: store.head(env.root, 'nora')!.fingerprint })
71
+ expect(pack.sections.find((entry) => entry.id === 'system')!.sources).toEqual([{ id: 'nora', kind: 'harness', revision: pack.harness!.fingerprint }])
102
72
  })
103
73
 
104
- test('long rules shorten only the rules section', () => {
74
+ test('without a tree, only the application, the Wiki and the tools speak', () => {
105
75
  const env = setup()
106
- writeFileSync(join(env.root, 'registry/rules/tests.md'), `---\nname: Run tests\n---\n\n${'Always run the tests.\n\n'.repeat(600)}\n`)
107
- const pack = compile(env, session('p1'))
108
- const rules = pack.sections.find((entry) => entry.id === 'rules')!
109
- expect(rules.cut && rules.size <= 3000 && rules.body.endsWith('wiki_list kind rule.)')).toBe(true)
110
- expect(ids(pack)).toEqual(['blocks', 'rules', 'skills', 'knowledge', 'tools'])
111
- expect(pack.sections.filter((entry) => entry.id !== 'rules').every((entry) => !entry.cut)).toBe(true)
76
+ const pack = compile(env, agent(), hosted())
77
+ expect(pack.sections.map((entry) => entry.id)).toEqual(['identity', 'work', 'wiki', 'tools'])
78
+ expect(pack.harness).toBeNull()
112
79
  })
113
80
 
114
- test('an agent gets the application sections, its blocks and the tool groups in order', () => {
81
+ test('the Wiki section lists the current project\'s notes and only describes wiki_write when it is on', () => {
115
82
  const env = setup()
116
- const pack = compile(env, agent({ runId: 'run-1' }), hosted({
117
- input: 'How does Astro render the site?',
118
- tools: ['dispatch_work', 'list_agents', 'read'],
119
- toolGroups: [
120
- { group: 'Work', tools: ['dispatch_work'], guidance: 'Work (dispatch_work): record and dispatch substantial tasks.', order: 30 },
121
- { group: 'Agents', tools: ['list_agents', 'get_agent'], guidance: 'Agents (list_agents, get_agent): see and message the other agents.', order: 40 },
122
- { group: 'Browser', tools: [], guidance: 'Browser: act in the managed browser.', order: 50 },
123
- { group: 'Files', tools: ['files_list', 'files_read'], guidance: 'Saved files (files_list, files_read): read the shared vault.', order: 80 }
124
- ]
125
- }))
126
- expect(pack.toolGroups.map((group) => group.group)).toEqual(['Memory', 'Knowledge', 'Work', 'Agents', 'Native', 'Files'])
127
- expect(pack.toolGroups[3].tools).toEqual(['list_agents', 'get_agent'])
128
- const tools = pack.sections.at(-1)!.body
129
- expect(tools).toContain('Work (dispatch_work)')
130
- expect(tools).toContain('Files (read)')
131
- expect(tools).not.toContain('Browser')
132
- expect(tools).not.toContain('connected apps')
133
- expect(pack.profile).toBe('agent')
134
- expect(ids(pack)).toEqual(['identity', 'role', 'blocks', 'rules', 'skills', 'work', 'knowledge', 'tools'])
135
- const text = render(env, pack)
136
- for (const expected of ['# Xingular Harness context\n\nYou are "Nora", an Xingular Agent agent.\n\n## Role', '<block label="persona"', '<block label="notes"', 'You have no unresolved work.', '- /projects/xingular-agent', 'Site uses Astro']) expect(text).toContain(expected)
137
- // A volatile fact is never carried, even when it matches.
138
- expect(text).not.toContain('On call this week')
139
- // A skill enabled for one project is not listed for an agent, only pointed to.
140
- expect(text).not.toContain('review (skills/review)')
83
+ const text = render(env, compile(env, agent(), hosted()))
84
+ expect(text).toContain('- projects/xingular-agent/decisions — Decisions')
85
+ expect(text).toContain('wiki_search, wiki_read and wiki_list read project notes.')
86
+ expect(text).not.toContain('wiki_write')
87
+ expect(render(env, compile(env, agent(), hosted({ tools: ['read', 'wiki_write'] })))).toContain('wiki_write saves one')
141
88
  })
142
89
 
143
- test('a running session is told once when a block changes', () => {
90
+ test('selected notes are references for this request only', () => {
144
91
  const env = setup()
145
- const directory = mkdtempSync(join(tmpdir(), 'harness-session-'))
146
- saveForSession(directory, compile(env, session('p1')))
147
- expect(memoryDelta(env, directory, session('p1'))).toBeNull()
148
- blocks.edit(env.root, { kind: 'user' }, 'project/xingular-agent', { kind: 'append', text: 'Uses pnpm.' })
149
- const note = memoryDelta(env, directory, session('p1'))!
150
- expect(note.startsWith('[Memory updated since')).toBe(true)
151
- expect(note).toContain('<block label="project/xingular-agent"')
152
- expect(note).toContain('Uses pnpm.')
153
- expect(note).not.toContain('<block label="user"')
154
- expect(memoryDelta(env, directory, session('p1'))).toBeNull()
155
- })
156
-
157
- test('a profile limits the sections and is recorded', () => {
158
- const env = setup()
159
- expect(profileFor(env.root, session('p1'))).toBe('full')
160
- setProfile(env.root, 's1', 'minimal')
161
- const pack = compile(env, session('p1'))
162
- expect([pack.contextProfile, ids(pack)]).toEqual(['minimal', ['rules', 'tools']])
163
- setProfile(env.root, 's1', 'off')
164
- const off = compile(env, session('p1'))
165
- expect([off.sections, off.toolGroups]).toEqual([[], []])
166
- setProfile(env.root, 's1', 'full')
167
- expect(profiles(env.root)).toEqual({})
168
- expect(() => setProfile(env.root, '../x', 'off')).toThrow()
169
- })
170
-
171
- test('the pack is recorded with the run', () => {
172
- const env = setup()
173
- const text = launchText(env, session('p1'))
174
- const saved = load(env.root, 'run-1')!
175
- expect(render(env, saved)).toBe(text)
176
- expect(saved.sections.some((entry) => entry.id === 'blocks' && entry.sources.some((source) => source.id === 'blocks/shared/user'))).toBe(true)
177
- expect(readRunContext(env.root, 'run-1').some((entry) => entry.id === 'rules/tests' && entry.via === 'entrypoint')).toBe(true)
178
- expect(load(env.root, 'missing')).toBeNull()
92
+ const launch = hosted({ references: [{ kind: 'note', path: 'library/glossary' }, { kind: 'line', id: 'l1', label: 'Line', text: 'The user pinned a screenshot.' }] })
93
+ const section = compile(env, agent(), launch).sections.find((entry) => entry.id === 'references')!
94
+ expect(section.body).toContain('The user pinned a screenshot.')
95
+ expect(section.body).toContain('A harness is the system around a model.')
96
+ expect(section.sources[0]).toMatchObject({ id: 'library/glossary', kind: 'note' })
97
+ expect(SECTION_ORDER.references).toBeLessThan(SECTION_ORDER.wiki)
179
98
  })
180
99
 
181
100
  function delivered(path: string, update: ContextUpdate): void {
@@ -184,64 +103,28 @@ function delivered(path: string, update: ContextUpdate): void {
184
103
 
185
104
  test('state stays out of the system prompt and only delivered changes are resent', () => {
186
105
  const env = setup()
187
- const caller = agent({ runId: 'run-1' })
188
- const first = sessionLaunch(env, caller, hosted(), null)
189
- const initial = first.update!
190
- expect(first.system).not.toContain('<block')
106
+ const materialised = scoped(env)
107
+ const first = sessionLaunch(env, agent(), hosted(), null, materialised)
108
+ expect(first.system).toContain('You draft release notes.')
191
109
  expect(first.system).not.toContain('You have no unresolved work.')
192
- expect(first.system.endsWith('through tools when needed.\n')).toBe(true)
193
- expect(initial.content).toContain('<block')
194
- expect(initial.content).toContain('You have no unresolved work.')
195
- // Recording a prepared launch is not evidence that the session received its context.
196
- expect(sessionLaunch(env, caller, hosted(), null).update).not.toBeNull()
110
+ expect(first.update!.content).toContain('You have no unresolved work.')
111
+ expect(first.update!.content).toContain('## Wiki')
197
112
  const history = join(mkdtempSync(join(tmpdir(), 'harness-history-')), 'chat.jsonl')
198
- delivered(history, initial)
199
- const unchanged = sessionLaunch(env, caller, hosted(), history)
113
+ delivered(history, first.update!)
114
+ const unchanged = sessionLaunch(env, agent(), hosted(), history, materialised)
200
115
  expect(unchanged.system).toBe(first.system)
201
116
  expect(unchanged.update).toBeNull()
202
- blocks.edit(env.root, { kind: 'user' }, 'user', { kind: 'append', text: 'New shared preference.' })
203
- const changed = sessionLaunch(env, caller, hosted(), history)
117
+ notes.write(env.root, 'projects/xingular-agent/release', '# Release checklist\n')
118
+ const changed = sessionLaunch(env, agent(), hosted(), history, materialised)
204
119
  expect(changed.system).toBe(first.system)
205
- expect(changed.update!.content).toContain('New shared preference.')
120
+ expect(changed.update!.content).toContain('projects/xingular-agent/release')
206
121
  expect(changed.update!.content).not.toContain('## Active work')
207
- expect(changed.update!.content).not.toContain('## Knowledge')
208
- expect(load(env.root, 'run-1')!.delivery!.updatedSectionIds).toEqual(['blocks'])
209
- writeFileSync(join(env.root, 'registry/rules/tests.md'), '---\nname: Run tests\n---\n\nFollow the updated rule.\n')
210
- const updated = sessionLaunch(env, caller, hosted(), history).system
211
- expect(updated).not.toBe(first.system)
212
- expect(updated).toContain('Follow the updated rule.')
213
- })
214
-
215
- test('pinned application text reaches native history with context off without changing the system prefix', () => {
216
- const env = setup()
217
- setProfile(env.root, 'nora', 'off')
218
- const before = sessionLaunch(env, agent(), hosted(), null)
219
- const after = sessionLaunch(env, agent(), { sections: [identity, role, work('You have no unresolved work.\n', 'Recent task reports:\n\nTask t1: Reviewed evidence shown to the user\n')] }, null)
220
- expect(after.system).toBe(before.system)
221
- expect(after.update!.content).toContain('Reviewed evidence shown to the user')
222
- expect(after.update!.content).not.toContain('You have no unresolved work.')
223
- expect(before.update!.content).not.toContain('Reviewed evidence')
224
- })
225
-
226
- test('references apply to each request and profile changes withdraw old state', () => {
227
- const env = setup()
228
- const revision = references.pin(env.root, 'skill', 'skills/review')
229
- const launch = hosted({ references: [{ kind: 'skill', id: 'skills/review', label: 'Review', revision }] })
230
- const first = sessionLaunch(env, agent(), launch, null)
231
- expect(first.system).not.toContain('Read the diff first.')
232
- const history = join(mkdtempSync(join(tmpdir(), 'harness-history-')), 'chat.jsonl')
233
- delivered(history, first.update!)
234
- const repeated = sessionLaunch(env, agent(), launch, history).update!
235
- expect(repeated.content).toContain('Read the diff first.')
236
- expect(repeated.content).not.toContain('## Memory blocks')
237
- const removed = sessionLaunch(env, agent(), hosted(), history).update!
238
- expect(removed.content).toContain('## references')
239
- expect(removed.content).not.toContain('Read the diff first.')
240
- setProfile(env.root, 'nora', 'off')
241
- const withdrawn = sessionLaunch(env, agent(), hosted(), history).update!
242
- expect(withdrawn.snapshot).toEqual([])
243
- for (const id of ['blocks', 'references', 'work', 'knowledge']) expect(withdrawn.content).toContain(`## ${id}`)
244
- expect(sessionLaunch(env, agent(), launch, history).update!.content).toContain('Read the diff first.')
122
+ expect(load(env.root, 'run-1')!.delivery!.updatedSectionIds).toEqual(['wiki'])
123
+ // A new harness version changes the stable prefix, never the delivered state.
124
+ store.commit(env.root, 'nora', { ...tree, 'memory.md': 'Releases ship on Thursdays.\n' }, { kind: 'edit' })
125
+ const next = sessionLaunch(env, agent(), hosted(), history, materialise(env.root, { scope: 'nora' }, { subagents: true }))
126
+ expect(next.system).toContain('Releases ship on Thursdays.')
127
+ expect(next.system).not.toBe(first.system)
245
128
  })
246
129
 
247
130
  test('the snapshot follows the active branch and refreshes after compaction', () => {
@@ -251,11 +134,17 @@ test('the snapshot follows the active branch and refreshes after compaction', ()
251
134
  const history = join(directory, 'chat.jsonl')
252
135
  delivered(history, initial)
253
136
  const original = readFileSync(history, 'utf8')
254
- // An unrelated branch's snapshot must not decide what this branch already received.
255
137
  writeFileSync(history, `${original}${JSON.stringify({ type: 'custom_message', id: 'other', parentId: null, customType: 'xingular-context', details: { xingularContext: [] } })}\n${JSON.stringify({ type: 'message', id: 'leaf', parentId: 'context-1' })}\n`)
256
138
  expect(sessionLaunch(env, agent(), hosted(), history).update).toBeNull()
257
139
  writeFileSync(history, `${original}${JSON.stringify({ type: 'compaction', id: 'summary', parentId: 'context-1' })}\n`)
258
- expect(sessionLaunch(env, agent(), hosted(), history).update!.content).toContain('## Memory blocks')
140
+ expect(sessionLaunch(env, agent(), hosted(), history).update!.content).toContain('## Wiki')
259
141
  expect(sessionFile(directory, join(directory, 'older.jsonl'))).toBe(join(directory, 'older.jsonl'))
260
142
  expect(sessionFile(directory)).toBe(history)
261
143
  })
144
+
145
+ test('a subagent section is left out when the session cannot start subagents', () => {
146
+ const env = setup()
147
+ store.seed(env.root, 'nora', tree)
148
+ const pack = compile(env, agent(), hosted(), materialise(env.root, { scope: 'nora' }, { subagents: false }))
149
+ expect(pack.sections.some((entry) => entry.id === 'subagents')).toBe(false)
150
+ })