@xingular/harness 0.0.0-stage → 0.1.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 (133) hide show
  1. package/README.md +269 -2
  2. package/dist/cli.js +11046 -0
  3. package/dist/terminal-pi.js +326 -0
  4. package/package.json +41 -4
  5. package/src/accounts/accounts.test.ts +223 -0
  6. package/src/accounts/accounts.ts +522 -0
  7. package/src/accounts/env.ts +204 -0
  8. package/src/accounts/secrets.ts +50 -0
  9. package/src/accounts/store.ts +338 -0
  10. package/src/capabilities/api.ts +75 -0
  11. package/src/capabilities/runSnapshot.ts +59 -0
  12. package/src/capabilities/types.ts +34 -0
  13. package/src/cli.ts +110 -0
  14. package/src/core/bus.ts +34 -0
  15. package/src/core/changes.ts +33 -0
  16. package/src/core/env.ts +27 -0
  17. package/src/core/errors.ts +32 -0
  18. package/src/core/frontmatter.ts +47 -0
  19. package/src/core/fs.test.ts +38 -0
  20. package/src/core/fs.ts +92 -0
  21. package/src/core/index.ts +193 -0
  22. package/src/core/path.ts +45 -0
  23. package/src/core/projects.ts +90 -0
  24. package/src/core/text.ts +39 -0
  25. package/src/harness.ts +289 -0
  26. package/src/index.ts +69 -0
  27. package/src/mcp/connections.ts +441 -0
  28. package/src/mcp/manager.ts +130 -0
  29. package/src/mcp/oauth.ts +195 -0
  30. package/src/mcp/service.test.ts +376 -0
  31. package/src/mcp/service.ts +567 -0
  32. package/src/mcp/types.ts +105 -0
  33. package/src/memory/api.test.ts +82 -0
  34. package/src/memory/api.ts +287 -0
  35. package/src/memory/blocks.test.ts +78 -0
  36. package/src/memory/blocks.ts +406 -0
  37. package/src/memory/caller.ts +57 -0
  38. package/src/memory/dispatch.ts +59 -0
  39. package/src/memory/facts.test.ts +41 -0
  40. package/src/memory/facts.ts +248 -0
  41. package/src/memory/graph.ts +103 -0
  42. package/src/memory/health.test.ts +130 -0
  43. package/src/memory/health.ts +74 -0
  44. package/src/memory/maintainer.ts +272 -0
  45. package/src/memory/maintenance.test.ts +176 -0
  46. package/src/memory/maintenance.ts +330 -0
  47. package/src/memory/migrate.ts +79 -0
  48. package/src/memory/operations.ts +138 -0
  49. package/src/memory/proposals.test.ts +168 -0
  50. package/src/memory/proposals.ts +576 -0
  51. package/src/memory/recall.test.ts +104 -0
  52. package/src/memory/recall.ts +271 -0
  53. package/src/memory/references.ts +77 -0
  54. package/src/memory/runContext.ts +93 -0
  55. package/src/memory/secrets.ts +6 -0
  56. package/src/memory/store.test.ts +66 -0
  57. package/src/memory/store.ts +385 -0
  58. package/src/memory/wiki.test.ts +47 -0
  59. package/src/memory/wiki.ts +422 -0
  60. package/src/memory/wikiIndex.test.ts +88 -0
  61. package/src/memory/wikiIndex.ts +116 -0
  62. package/src/plugins/api.ts +181 -0
  63. package/src/plugins/attachments.test.ts +84 -0
  64. package/src/plugins/attachments.ts +128 -0
  65. package/src/plugins/builtin.ts +82 -0
  66. package/src/plugins/catalogue.test.ts +254 -0
  67. package/src/plugins/catalogue.ts +250 -0
  68. package/src/plugins/cli.ts +184 -0
  69. package/src/plugins/discover.ts +151 -0
  70. package/src/plugins/firstParty.test.ts +143 -0
  71. package/src/plugins/firstParty.ts +129 -0
  72. package/src/plugins/hooks.test.ts +250 -0
  73. package/src/plugins/hooks.ts +239 -0
  74. package/src/plugins/identity.ts +37 -0
  75. package/src/plugins/install.test.ts +107 -0
  76. package/src/plugins/install.ts +106 -0
  77. package/src/plugins/inventory.ts +123 -0
  78. package/src/plugins/manifest.ts +44 -0
  79. package/src/plugins/plugins.test.ts +205 -0
  80. package/src/plugins/plugins.ts +184 -0
  81. package/src/plugins/proposal.ts +121 -0
  82. package/src/plugins/runtime.test.ts +40 -0
  83. package/src/plugins/runtime.ts +82 -0
  84. package/src/plugins/trust.ts +142 -0
  85. package/src/plugins/types.ts +130 -0
  86. package/src/prompt/compile.test.ts +261 -0
  87. package/src/prompt/compile.ts +732 -0
  88. package/src/server/methods.ts +308 -0
  89. package/src/server/remote.ts +77 -0
  90. package/src/server/rpc.ts +116 -0
  91. package/src/server/serve.ts +28 -0
  92. package/src/server/server.test.ts +174 -0
  93. package/src/sessions/approvals.ts +118 -0
  94. package/src/sessions/contracts.ts +299 -0
  95. package/src/sessions/conversation.test.ts +150 -0
  96. package/src/sessions/extensionSession.test.ts +74 -0
  97. package/src/sessions/extensionUi.test.ts +65 -0
  98. package/src/sessions/extensionUi.ts +102 -0
  99. package/src/sessions/history.ts +51 -0
  100. package/src/sessions/lineageJoin.ts +75 -0
  101. package/src/sessions/policy.ts +32 -0
  102. package/src/sessions/pricing.ts +60 -0
  103. package/src/sessions/reasoningSummaries.ts +59 -0
  104. package/src/sessions/registry.ts +294 -0
  105. package/src/sessions/runIndex.ts +99 -0
  106. package/src/sessions/runStore.test.ts +152 -0
  107. package/src/sessions/runStore.ts +332 -0
  108. package/src/sessions/runner.ts +556 -0
  109. package/src/sessions/runtimeResources.test.ts +61 -0
  110. package/src/sessions/sessions.test.ts +434 -0
  111. package/src/sessions/sessions.ts +295 -0
  112. package/src/skills/registry.test.ts +104 -0
  113. package/src/skills/registry.ts +394 -0
  114. package/src/terminal/cli.ts +149 -0
  115. package/src/terminal/client.ts +74 -0
  116. package/src/terminal/controller.ts +227 -0
  117. package/src/terminal/piExtension.ts +208 -0
  118. package/src/terminal/pluginControl.test.ts +146 -0
  119. package/src/terminal/pluginMenu.ts +112 -0
  120. package/src/terminal/socket.ts +182 -0
  121. package/src/terminal/standalone.test.ts +115 -0
  122. package/src/terminal/standalone.ts +126 -0
  123. package/src/terminal/terminal.test.ts +302 -0
  124. package/src/terminal/terminals.ts +429 -0
  125. package/src/tools/askUser.ts +45 -0
  126. package/src/tools/definitions.ts +187 -0
  127. package/src/tools/memoryTools.ts +22 -0
  128. package/src/tools/pluginPropose.ts +41 -0
  129. package/src/tools/types.ts +36 -0
  130. package/src/web/safeRequest.ts +171 -0
  131. package/src/web/text.ts +53 -0
  132. package/src/web/tools.ts +134 -0
  133. package/src/web/web.test.ts +141 -0
@@ -0,0 +1,130 @@
1
+ // What a person sees of everything that shapes a session. A plugin is a bundle with a title and
2
+ // a description; its contributions are the instructions, skills, commands, tools, connections,
3
+ // code and themes it brings.
4
+ import type { Attachment } from './attachments'
5
+ import type { Registered } from './plugins'
6
+ import type { ProjectCode } from './trust'
7
+
8
+ export const CONTRIBUTION_KINDS = ['instruction', 'skill', 'command', 'tool', 'connection', 'code', 'theme'] as const
9
+ export type ContributionKind = (typeof CONTRIBUTION_KINDS)[number]
10
+
11
+ /**
12
+ * Where a plugin comes from: the harness itself, the embedding application, Pi, a person's own
13
+ * registry, the folder every account shares, an installed package, the working folder, or the
14
+ * home folder.
15
+ */
16
+ export const PLUGIN_SOURCES = ['builtin', 'application', 'pi', 'yours', 'shared', 'package', 'project', 'home'] as const
17
+ export type PluginSource = (typeof PLUGIN_SOURCES)[number]
18
+
19
+ /** `untrusted` is code that is found and does not load until a person trusts it. */
20
+ export type ContributionState = 'on' | 'off' | 'untrusted'
21
+
22
+ export interface Contribution {
23
+ /** `<plugin id>/<kind>/<name>`. */
24
+ id: string
25
+ kind: ContributionKind
26
+ name: string
27
+ description: string
28
+ path: string | null
29
+ state: ContributionState
30
+ /** Who it is switched on for, in words: "Everywhere", a project's name, "2 agents". */
31
+ scope: string | null
32
+ /** Where it is switched on and off, or `null` when it cannot be switched on its own. */
33
+ attachment: Attachment | null
34
+ /** What a code contribution registered the last time a session loaded it. */
35
+ registered: Registered | null
36
+ /** What went wrong the last time it ran, for code the harness runs itself. */
37
+ problem: string | null
38
+ /** Code the harness runs itself after a change, for everyone, outside any session. */
39
+ background: boolean
40
+ }
41
+
42
+ /** Where a person changes a plugin today. */
43
+ export type PluginManage = 'registry' | 'tools' | 'connections' | 'files'
44
+
45
+ export interface Plugin {
46
+ id: string
47
+ title: string
48
+ description: string
49
+ version: string | null
50
+ source: PluginSource
51
+ path: string | null
52
+ runsCode: boolean
53
+ trust: 'none' | 'trusted' | 'untrusted'
54
+ /** The code a person is asked to trust, for a working folder's own. */
55
+ code: ProjectCode | null
56
+ /**
57
+ * How a person installed it, when it can be removed here: named in the settings every account
58
+ * shares (`source` is the name there), or a folder kept with their data (`source` is its path).
59
+ */
60
+ install: { kind: 'settings' | 'folder'; source: string } | null
61
+ /** The parts it adds to, by plugin id: a package that says it extends Memory, the Wiki or History. */
62
+ extends: string[]
63
+ /** Why it cannot be removed, or `null` when it can. */
64
+ required: string | null
65
+ manage: PluginManage | null
66
+ /** Numbers a person can set on it, each with its current value. */
67
+ settings: (PluginSetting & { value: number })[]
68
+ /** Where the whole plugin is switched on and off, or `null` when only its parts are, or nothing is. */
69
+ attachment: Attachment | null
70
+ contributions: Contribution[]
71
+ }
72
+
73
+ /** A number a person can set on a plugin, such as a limit. */
74
+ export interface PluginSetting {
75
+ key: string
76
+ label: string
77
+ description: string
78
+ default: number
79
+ min: number
80
+ max: number
81
+ }
82
+
83
+ /** What a part of the harness, or the embedding application, says about itself. Data only. */
84
+ export interface PluginDescriptor {
85
+ id: string
86
+ title: string
87
+ description: string
88
+ /** Why it cannot be removed. */
89
+ required?: string
90
+ manage?: PluginManage
91
+ tools?: { name: string; description?: string }[]
92
+ /** The prompt sections it writes. */
93
+ sections?: { id: string; name: string; description?: string }[]
94
+ /** The tool groups whose paragraph in the tools section it writes. */
95
+ toolGroups?: string[]
96
+ /** It ships with the harness and a person may switch it. `on` is its state until they do. */
97
+ switchable?: { on: boolean }
98
+ /** Numbers a person can set on it. */
99
+ settings?: PluginSetting[]
100
+ /** Code of its own that runs in a session. */
101
+ code?: { name: string; description: string }[]
102
+ }
103
+
104
+ export type Delivery = 'system' | 'context' | 'tool' | 'on-demand' | 'code' | 'background'
105
+
106
+ /** One thing that reaches a caller. */
107
+ export interface InventoryItem {
108
+ pluginId: string
109
+ contributionId: string | null
110
+ kind: ContributionKind
111
+ name: string
112
+ /** `system` is the stable prefix, `context` an update sent with a request, `on-demand` text a tool loads, `background` code the harness runs after a change. */
113
+ delivery: Delivery
114
+ /** Characters it adds to what the model is told, when that is known. */
115
+ chars: number | null
116
+ /** Its text was cut to fit a budget. */
117
+ cut: boolean
118
+ }
119
+
120
+ export interface Inventory {
121
+ /** The plugins the items belong to. */
122
+ plugins: Plugin[]
123
+ /** Every other plugin, with nothing reaching this caller: what could still be switched on for it. */
124
+ others: Plugin[]
125
+ items: InventoryItem[]
126
+ /** Prompt sections, tool groups and tools that no plugin accounts for. */
127
+ unattributed: { kind: 'section' | 'toolGroup' | 'tool'; id: string }[]
128
+ systemChars: number
129
+ contextChars: number
130
+ }
@@ -0,0 +1,261 @@
1
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, expect, test } from 'vitest'
5
+
6
+ import { dataRoot, writeFile } from '../../tests/helpers'
7
+ import { createEnv, type HarnessEnv } from '../core/env'
8
+ import { writeJson } from '../core/fs'
9
+ 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'
17
+
18
+ const roots: string[] = []
19
+ afterEach(() => roots.splice(0).forEach(closeIndex))
20
+
21
+ function setup(): HarnessEnv {
22
+ const root = dataRoot()
23
+ 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'] })
37
+ mkdirSync(join(root, 'runs/run-1'), { recursive: true })
38
+ reindexWiki(root)
39
+ return createEnv(root)
40
+ }
41
+
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)
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 })
51
+
52
+ test('cuts at a paragraph and points to the rest', () => {
53
+ expect(fit('short', 100, '(more)')).toEqual({ body: 'short', cut: false })
54
+ const cut = fit(`${'a'.repeat(40)}\n\n${'b'.repeat(40)}\n\n${'c'.repeat(40)}`, 100, '(more)')
55
+ expect(cut.cut).toBe(true)
56
+ expect(cut.body).toBe(`${'a'.repeat(40)}\n\n${'b'.repeat(40)}\n\n(more)`)
57
+ })
58
+
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', () => {
79
+ 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'])
96
+ 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('agentx-wiki search')
101
+ expect(cli).toContain('agentx-wiki block append')
102
+ })
103
+
104
+ test('long rules shorten only the rules section', () => {
105
+ 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)
112
+ })
113
+
114
+ test('an agent gets the application sections, its blocks and the tool groups in order', () => {
115
+ 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)')
141
+ })
142
+
143
+ test('a running session is told once when a block changes', () => {
144
+ 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()
179
+ })
180
+
181
+ function delivered(path: string, update: ContextUpdate): void {
182
+ writeFileSync(path, `${JSON.stringify({ type: 'session', id: 'native-chat' })}\n${JSON.stringify({ type: 'custom_message', id: 'context-1', parentId: null, customType: 'xingular-context', content: update.content, details: { xingularContext: update.snapshot } })}\n`)
183
+ }
184
+
185
+ test('state stays out of the system prompt and only delivered changes are resent', () => {
186
+ 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')
191
+ 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()
197
+ const history = join(mkdtempSync(join(tmpdir(), 'harness-history-')), 'chat.jsonl')
198
+ delivered(history, initial)
199
+ const unchanged = sessionLaunch(env, caller, hosted(), history)
200
+ expect(unchanged.system).toBe(first.system)
201
+ 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)
204
+ expect(changed.system).toBe(first.system)
205
+ expect(changed.update!.content).toContain('New shared preference.')
206
+ 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.')
245
+ })
246
+
247
+ test('the snapshot follows the active branch and refreshes after compaction', () => {
248
+ const env = setup()
249
+ const initial = sessionLaunch(env, agent(), hosted(), null).update!
250
+ const directory = mkdtempSync(join(tmpdir(), 'harness-history-'))
251
+ const history = join(directory, 'chat.jsonl')
252
+ delivered(history, initial)
253
+ const original = readFileSync(history, 'utf8')
254
+ // An unrelated branch's snapshot must not decide what this branch already received.
255
+ 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
+ expect(sessionLaunch(env, agent(), hosted(), history).update).toBeNull()
257
+ 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')
259
+ expect(sessionFile(directory, join(directory, 'older.jsonl'))).toBe(join(directory, 'older.jsonl'))
260
+ expect(sessionFile(directory)).toBe(history)
261
+ })