thinkpool-pair 0.7.363 → 0.7.365
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.
- package/LICENSE +18 -0
- package/README.md +20 -0
- package/abort-turn-barrier.mjs +1 -23
- package/account.mjs +1 -1437
- package/acp-client.mjs +1 -140
- package/agent-detect.mjs +1 -28
- package/agent-notify.mjs +1 -142
- package/agent-visibility.mjs +1 -67
- package/auth-store.mjs +1 -120
- package/bridge.mjs +1 -6240
- package/byok-detect.mjs +1 -126
- package/claude-command-catalog.mjs +1 -91
- package/claude-session.mjs +1 -1519
- package/code-event-contract.mjs +1 -118
- package/codex-app-server.mjs +1 -340
- package/codex-commands.mjs +1 -83
- package/codex-event-mapper.mjs +1 -232
- package/codex-images.mjs +1 -69
- package/codex-mcp-http.mjs +1 -131
- package/codex-session.mjs +1 -1295
- package/command-catalog.mjs +1 -118
- package/command-guidance.mjs +1 -8
- package/context-contract.mjs +1 -95
- package/context-windows.mjs +1 -107
- package/cross-terminal.mjs +1 -789
- package/cumulative-event-relay.mjs +1 -53
- package/design-edit.mjs +1 -424
- package/design-source-contract.mjs +1 -4
- package/direct-pair-room.mjs +1 -57
- package/dispatch-lease.mjs +1 -37
- package/dispatch-permission-cleanup.mjs +1 -86
- package/edit-diff.mjs +1 -136
- package/error-recovery.mjs +1 -50
- package/event-bounds.mjs +1 -121
- package/event-delivery-queue.mjs +1 -0
- package/event-id.mjs +1 -549
- package/evidence-citations.mjs +1 -50
- package/evidence-compact.mjs +1 -11
- package/flow-assembly.mjs +1 -196
- package/flow-budget.mjs +1 -84
- package/flow-conductor.mjs +1 -259
- package/flow-context-store.mjs +1 -387
- package/flow-host-revert.mjs +1 -42
- package/flow-models.mjs +1 -139
- package/flow-preview.mjs +1 -148
- package/flow-receipt.mjs +1 -122
- package/flow-redispatch.mjs +1 -72
- package/flow-review-gate.mjs +1 -402
- package/flow-review-reflect.mjs +1 -115
- package/flow-review.mjs +1 -152
- package/flow-scope-evidence.mjs +1 -117
- package/flow-skill-registry.mjs +1 -140
- package/flow-task-graph.mjs +1 -562
- package/flow-worktree.mjs +1 -71
- package/git-diff-report.mjs +1 -121
- package/hermes-delegation-guard.mjs +1 -14
- package/hermes-event-mapper.mjs +1 -194
- package/hermes-isolation.mjs +1 -61
- package/hermes-model-cache.mjs +1 -54
- package/hermes-policy.mjs +1 -91
- package/hermes-probe.mjs +1 -57
- package/hermes-session.mjs +1 -683
- package/hermes-setup.mjs +1 -167
- package/host-memory.mjs +1 -116
- package/interrupted-resume.mjs +1 -95
- package/keep-awake.mjs +1 -148
- package/key-shape.mjs +1 -49
- package/lane-continuation.mjs +1 -83
- package/lane-lifecycle.mjs +1 -189
- package/lane-worktree.mjs +1 -77
- package/launcher.mjs +1 -354
- package/mcp-flight-recorder.mjs +1 -0
- package/mockup-delivery.mjs +1 -57
- package/model-prices.mjs +1 -113
- package/package.json +15 -4
- package/pair-bus.mjs +1 -98
- package/pair-control-authority.mjs +1 -89
- package/past-work-search.mjs +1 -105
- package/plan-meters.mjs +1 -144
- package/presence.mjs +1 -191
- package/privacy-report.mjs +1 -108
- package/provider-resilience.mjs +1 -356
- package/provider.mjs +1 -133
- package/providers.mjs +1 -491
- package/publish-guard.mjs +2 -0
- package/publish-manifest.json +129 -0
- package/question-response.mjs +1 -58
- package/reap-terminal.mjs +1 -68
- package/recap.mjs +1 -297
- package/replay-transport.mjs +1 -64
- package/repo-search.mjs +1 -190
- package/review-check.mjs +1 -182
- package/runtime-contract.mjs +1 -93
- package/runtime-registry.mjs +1 -64
- package/runtime-session.mjs +1 -20
- package/scheduled-run-admission.mjs +1 -364
- package/scheduled-runs.mjs +1 -268
- package/sdk-admission.mjs +1 -9
- package/sdk-smoke.mjs +1 -61
- package/serve-consent.mjs +1 -118
- package/serve-dir.mjs +1 -40
- package/service.mjs +1 -877
- package/session-store.mjs +1 -484
- package/side-lane.mjs +1 -63
- package/supabase-key.mjs +1 -176
- package/supervisor-ready.mjs +1 -57
- package/switch-provider.mjs +1 -84
- package/terminal-name.mjs +1 -359
- package/terminal-row-reconcile.mjs +1 -70
- package/thinkpool-capabilities.json +11 -2
- package/thinkpool-prompt-contracts.mjs +1 -85
- package/thinkpool-room-prompt.mjs +1 -187
- package/transcript-sanitize.mjs +1 -332
- package/turn-stall.mjs +1 -61
- package/update-gate.mjs +1 -53
- package/viewport.mjs +1 -810
- package/worker-completion.mjs +1 -57
package/flow-worktree.mjs
CHANGED
|
@@ -1,71 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// A Flow task-lane builds its slice in its OWN worktree. A branch is NOT isolation
|
|
4
|
-
// when ≥2 lanes share one working tree (M57) — two lanes editing files in the shared
|
|
5
|
-
// checkout collide. A worktree gives each lane a disjoint tree + its own branch, and
|
|
6
|
-
// that branch is the atomic-revert target (commit_sha on flow_tasks, Step 4).
|
|
7
|
-
//
|
|
8
|
-
// Pure over injected git/fs so it's unit-testable against a temp repo.
|
|
9
|
-
// Spec: docs/specs/2026-06-29-thinkpool-flow.md (Step 2).
|
|
10
|
-
|
|
11
|
-
import { execFileSync } from 'node:child_process'
|
|
12
|
-
import path from 'node:path'
|
|
13
|
-
import fs from 'node:fs'
|
|
14
|
-
import { linkLocalEnvIntoWorktree } from './lane-worktree.mjs'
|
|
15
|
-
|
|
16
|
-
const ROOT = process.cwd() // the bridge's checkout (the shared main repo root)
|
|
17
|
-
|
|
18
|
-
// The lane's branch + directory for a (flowId, taskKey). Deterministic so a
|
|
19
|
-
// re-dispatch of the same task reuses the same worktree.
|
|
20
|
-
export function worktreeSpec ({ flowId, taskKey, root = ROOT }) {
|
|
21
|
-
const wtRoot = path.join(root, '.claude', 'worktrees')
|
|
22
|
-
const short = String(flowId).replace(/-/g, '').slice(0, 8)
|
|
23
|
-
const safeKey = String(taskKey).toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 40).replace(/^-+|-+$/g, '') || 'task'
|
|
24
|
-
const branch = `flow/${short}/${safeKey}`
|
|
25
|
-
const dir = path.join(wtRoot, `flow-${short}-${safeKey}`)
|
|
26
|
-
return { branch, dir, wtRoot }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Create a worktree for a task-lane off `base`. Idempotent: if the dir already holds a
|
|
30
|
-
// worktree, reuse it. Returns { dir, branch, created }.
|
|
31
|
-
//
|
|
32
|
-
// FL-M6/dispatch — the base must RESOLVE in this repo. The old hard default `origin/main`
|
|
33
|
-
// threw on any repo without an `origin` remote or a non-`main` default branch (e.g. a
|
|
34
|
-
// fresh `git init` checkout), and the dispatch loop swallowed the throw → NO lane ever
|
|
35
|
-
// spawned. Resolve the first ref that actually exists: caller's base → origin/main →
|
|
36
|
-
// origin/HEAD → main → master → HEAD.
|
|
37
|
-
export function createFlowWorktree ({ flowId, taskKey, base = null, root = ROOT, git = runGit, fsImpl = fs }) {
|
|
38
|
-
const { branch, dir, wtRoot } = worktreeSpec({ flowId, taskKey, root })
|
|
39
|
-
if (fsImpl.existsSync(path.join(dir, '.git'))) {
|
|
40
|
-
linkLocalEnvIntoWorktree({ root, dir, fsImpl })
|
|
41
|
-
return { dir, branch, created: false }
|
|
42
|
-
}
|
|
43
|
-
fsImpl.mkdirSync(wtRoot, { recursive: true })
|
|
44
|
-
let ref = base
|
|
45
|
-
if (!ref) {
|
|
46
|
-
for (const cand of ['origin/main', 'origin/HEAD', 'main', 'master', 'HEAD']) {
|
|
47
|
-
try { git(['rev-parse', '--verify', '--quiet', cand], root); ref = cand; break } catch { /* try next candidate */ }
|
|
48
|
-
}
|
|
49
|
-
ref = ref || 'HEAD'
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
git(['worktree', 'add', '-b', branch, dir, ref], root)
|
|
53
|
-
} catch {
|
|
54
|
-
// Branch already exists (a prior dispatch of this task) — check it out instead.
|
|
55
|
-
git(['worktree', 'add', dir, branch], root)
|
|
56
|
-
}
|
|
57
|
-
linkLocalEnvIntoWorktree({ root, dir, fsImpl })
|
|
58
|
-
return { dir, branch, created: true }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Remove a lane's worktree + delete its branch (on revert / cancel / flow cancel).
|
|
62
|
-
// Best-effort: missing worktree/branch is not an error.
|
|
63
|
-
export function removeFlowWorktree ({ flowId, taskKey, root = ROOT, git = runGit }) {
|
|
64
|
-
const { branch, dir } = worktreeSpec({ flowId, taskKey, root })
|
|
65
|
-
try { git(['worktree', 'remove', dir, '--force'], root) } catch { /* already gone */ }
|
|
66
|
-
try { git(['branch', '-D', branch], root) } catch { /* already gone */ }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function runGit (args, cwd) {
|
|
70
|
-
return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' })
|
|
71
|
-
}
|
|
1
|
+
import{execFileSync as r}from"node:child_process";import e from"node:path";import o from"node:fs";import{linkLocalEnvIntoWorktree as t}from"./lane-worktree.mjs";const c=process.cwd();export function worktreeSpec({flowId:r,taskKey:o,root:t=c}){const i=e.join(t,".claude","worktrees"),n=String(r).replace(/-/g,"").slice(0,8),a=String(o).toLowerCase().replace(/[^a-z0-9-]/g,"-").slice(0,40).replace(/^-+|-+$/g,"")||"task";return{branch:`flow/${n}/${a}`,dir:e.join(i,`flow-${n}-${a}`),wtRoot:i}}export function createFlowWorktree({flowId:r,taskKey:n,base:a=null,root:s=c,git:d=i,fsImpl:f=o}){const{branch:p,dir:l,wtRoot:w}=worktreeSpec({flowId:r,taskKey:n,root:s});if(f.existsSync(e.join(l,".git")))return t({root:s,dir:l,fsImpl:f}),{dir:l,branch:p,created:!1};f.mkdirSync(w,{recursive:!0});let k=a;if(!k){for(const r of["origin/main","origin/HEAD","main","master","HEAD"])try{d(["rev-parse","--verify","--quiet",r],s),k=r;break}catch{}k=k||"HEAD"}try{d(["worktree","add","-b",p,l,k],s)}catch{d(["worktree","add",l,p],s)}return t({root:s,dir:l,fsImpl:f}),{dir:l,branch:p,created:!0}}export function removeFlowWorktree({flowId:r,taskKey:e,root:o=c,git:t=i}){const{branch:n,dir:a}=worktreeSpec({flowId:r,taskKey:e,root:o});try{t(["worktree","remove",a,"--force"],o)}catch{}try{t(["branch","-D",n],o)}catch{}}function i(e,o){return r("git",e,{cwd:o,stdio:["ignore","pipe","pipe"],encoding:"utf8"})}
|
package/git-diff-report.mjs
CHANGED
|
@@ -1,121 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const DEFAULT_MAX_FILES = 24
|
|
4
|
-
const DEFAULT_MAX_COMMITS = 5
|
|
5
|
-
const DEFAULT_MAX_CHARS = 1800
|
|
6
|
-
|
|
7
|
-
const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`
|
|
8
|
-
|
|
9
|
-
function defaultRunGit(args, cwd) {
|
|
10
|
-
return execFileSync('git', args, {
|
|
11
|
-
cwd,
|
|
12
|
-
encoding: 'utf8',
|
|
13
|
-
timeout: 3000,
|
|
14
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
15
|
-
})
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function cleanLine(value, max = 240) {
|
|
19
|
-
return Array.from(String(value || ''), (character) => {
|
|
20
|
-
const code = character.charCodeAt(0)
|
|
21
|
-
const removableControl = (code <= 0x1f && code !== 0x09 && code !== 0x0a && code !== 0x0d) || code === 0x7f
|
|
22
|
-
return removableControl ? '' : character
|
|
23
|
-
}).join('')
|
|
24
|
-
.replace(/\s+/g, ' ')
|
|
25
|
-
.trim()
|
|
26
|
-
.slice(0, max)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function gitText(runGit, cwd, args) {
|
|
30
|
-
return String(runGit(args, cwd) || '').trim()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function tryGit(runGit, cwd, args) {
|
|
34
|
-
try { return gitText(runGit, cwd, args) } catch { return '' }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resolveComparisonRef(runGit, cwd) {
|
|
38
|
-
const remoteHead = tryGit(runGit, cwd, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'])
|
|
39
|
-
if (remoteHead && tryGit(runGit, cwd, ['rev-parse', '--verify', '--quiet', remoteHead])) return remoteHead
|
|
40
|
-
for (const candidate of ['origin/main', 'origin/master', 'main', 'master']) {
|
|
41
|
-
if (tryGit(runGit, cwd, ['rev-parse', '--verify', '--quiet', candidate])) return candidate
|
|
42
|
-
}
|
|
43
|
-
return ''
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function boundedReport(lines, maxChars) {
|
|
47
|
-
const kept = []
|
|
48
|
-
let length = 0
|
|
49
|
-
for (const raw of lines) {
|
|
50
|
-
const line = cleanLine(raw)
|
|
51
|
-
if (!line) continue
|
|
52
|
-
const nextLength = length + (kept.length ? 1 : 0) + line.length
|
|
53
|
-
if (nextLength > maxChars) {
|
|
54
|
-
const suffix = '… output shortened'
|
|
55
|
-
if (length + (kept.length ? 1 : 0) + suffix.length <= maxChars) kept.push(suffix)
|
|
56
|
-
break
|
|
57
|
-
}
|
|
58
|
-
kept.push(line)
|
|
59
|
-
length = nextLength
|
|
60
|
-
}
|
|
61
|
-
return kept.join('\n')
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function gitDiffReport({
|
|
65
|
-
cwd = process.cwd(),
|
|
66
|
-
runGit = defaultRunGit,
|
|
67
|
-
maxFiles = DEFAULT_MAX_FILES,
|
|
68
|
-
maxCommits = DEFAULT_MAX_COMMITS,
|
|
69
|
-
maxChars = DEFAULT_MAX_CHARS,
|
|
70
|
-
} = {}) {
|
|
71
|
-
gitText(runGit, cwd, ['rev-parse', '--show-toplevel'])
|
|
72
|
-
|
|
73
|
-
const status = tryGit(runGit, cwd, ['status', '--porcelain=v1', '--untracked-files=normal'])
|
|
74
|
-
const statusLines = status ? status.split(/\r?\n/).filter(Boolean) : []
|
|
75
|
-
const branch = tryGit(runGit, cwd, ['symbolic-ref', '--quiet', '--short', 'HEAD']) || 'detached HEAD'
|
|
76
|
-
const head = tryGit(runGit, cwd, ['rev-parse', '--short=8', 'HEAD']) || 'unknown'
|
|
77
|
-
const subject = cleanLine(tryGit(runGit, cwd, ['log', '-1', '--pretty=%s']), 160)
|
|
78
|
-
const comparisonRef = resolveComparisonRef(runGit, cwd)
|
|
79
|
-
const lines = []
|
|
80
|
-
|
|
81
|
-
if (statusLines.length) {
|
|
82
|
-
lines.push(`${plural(statusLines.length, 'uncommitted file')}`)
|
|
83
|
-
for (const line of statusLines.slice(0, Math.max(0, maxFiles))) lines.push(line)
|
|
84
|
-
if (statusLines.length > maxFiles) lines.push(`… ${plural(statusLines.length - maxFiles, 'more file')}`)
|
|
85
|
-
} else {
|
|
86
|
-
lines.push('No uncommitted changes')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
lines.push(`Branch · ${branch} · ${head}`)
|
|
90
|
-
|
|
91
|
-
if (!comparisonRef) {
|
|
92
|
-
if (subject) lines.push(`Latest commit · ${head} — ${subject}`)
|
|
93
|
-
return boundedReport(lines, maxChars)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const counts = tryGit(runGit, cwd, ['rev-list', '--left-right', '--count', `${comparisonRef}...HEAD`])
|
|
97
|
-
.split(/\s+/)
|
|
98
|
-
.map((value) => Number(value))
|
|
99
|
-
const behind = Number.isFinite(counts[0]) ? counts[0] : 0
|
|
100
|
-
const ahead = Number.isFinite(counts[1]) ? counts[1] : 0
|
|
101
|
-
|
|
102
|
-
if (ahead > 0) {
|
|
103
|
-
lines.push(`${plural(ahead, 'commit')} ahead of ${comparisonRef}${behind ? ` · ${plural(behind, 'commit')} behind` : ''}`)
|
|
104
|
-
const commitLines = tryGit(runGit, cwd, ['log', `--max-count=${Math.max(0, maxCommits)}`, '--pretty=%h %s', `${comparisonRef}..HEAD`])
|
|
105
|
-
.split(/\r?\n/)
|
|
106
|
-
.filter(Boolean)
|
|
107
|
-
if (commitLines.length) {
|
|
108
|
-
lines.push(`Commits not in ${comparisonRef}`)
|
|
109
|
-
lines.push(...commitLines)
|
|
110
|
-
if (ahead > commitLines.length) lines.push(`… ${plural(ahead - commitLines.length, 'more commit')}`)
|
|
111
|
-
}
|
|
112
|
-
} else if (behind > 0) {
|
|
113
|
-
if (subject) lines.push(`Latest commit · ${head} — ${subject} · already in ${comparisonRef}`)
|
|
114
|
-
lines.push(`${plural(behind, 'commit')} behind ${comparisonRef}`)
|
|
115
|
-
} else {
|
|
116
|
-
if (subject) lines.push(`Latest commit · ${head} — ${subject}`)
|
|
117
|
-
lines.push(`Up to date with ${comparisonRef}`)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return boundedReport(lines, maxChars)
|
|
121
|
-
}
|
|
1
|
+
import{execFileSync as t}from"node:child_process";const e=(t,e)=>`${t} ${e}${1===t?"":"s"}`;function n(e,n){return t("git",e,{cwd:n,encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","pipe"]})}function r(t,e=240){return Array.from(String(t||""),t=>{const e=t.charCodeAt(0);return e<=31&&9!==e&&10!==e&&13!==e||127===e?"":t}).join("").replace(/\s+/g," ").trim().slice(0,e)}function i(t,e,n){return String(t(n,e)||"").trim()}function o(t,e,n){try{return i(t,e,n)}catch{return""}}function s(t,e){const n=[];let i=0;for(const o of t){const t=r(o);if(!t)continue;const s=i+(n.length?1:0)+t.length;if(s>e){const t="… output shortened";i+(n.length?1:0)+t.length<=e&&n.push(t);break}n.push(t),i=s}return n.join("\n")}export function gitDiffReport({cwd:t=process.cwd(),runGit:u=n,maxFiles:c=24,maxCommits:m=5,maxChars:h=1800}={}){i(u,t,["rev-parse","--show-toplevel"]);const a=o(u,t,["status","--porcelain=v1","--untracked-files=normal"]),l=a?a.split(/\r?\n/).filter(Boolean):[],p=o(u,t,["symbolic-ref","--quiet","--short","HEAD"])||"detached HEAD",f=o(u,t,["rev-parse","--short=8","HEAD"])||"unknown",$=r(o(u,t,["log","-1","--pretty=%s"]),160),g=function(t,e){const n=o(t,e,["symbolic-ref","--quiet","--short","refs/remotes/origin/HEAD"]);if(n&&o(t,e,["rev-parse","--verify","--quiet",n]))return n;for(const n of["origin/main","origin/master","main","master"])if(o(t,e,["rev-parse","--verify","--quiet",n]))return n;return""}(u,t),d=[];if(l.length){d.push(`${e(l.length,"uncommitted file")}`);for(const t of l.slice(0,Math.max(0,c)))d.push(t);l.length>c&&d.push(`… ${e(l.length-c,"more file")}`)}else d.push("No uncommitted changes");if(d.push(`Branch · ${p} · ${f}`),!g)return $&&d.push(`Latest commit · ${f} — ${$}`),s(d,h);const v=o(u,t,["rev-list","--left-right","--count",`${g}...HEAD`]).split(/\s+/).map(t=>Number(t)),y=Number.isFinite(v[0])?v[0]:0,b=Number.isFinite(v[1])?v[1]:0;if(b>0){d.push(`${e(b,"commit")} ahead of ${g}${y?` · ${e(y,"commit")} behind`:""}`);const n=o(u,t,["log",`--max-count=${Math.max(0,m)}`,"--pretty=%h %s",`${g}..HEAD`]).split(/\r?\n/).filter(Boolean);n.length&&(d.push(`Commits not in ${g}`),d.push(...n),b>n.length&&d.push(`… ${e(b-n.length,"more commit")}`))}else y>0?($&&d.push(`Latest commit · ${f} — ${$} · already in ${g}`),d.push(`${e(y,"commit")} behind ${g}`)):($&&d.push(`Latest commit · ${f} — ${$}`),d.push(`Up to date with ${g}`));return s(d,h)}
|
|
@@ -1,15 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// has no per-session tool exclusion. Block it before execution so all delegated
|
|
4
|
-
// work remains a visible ThinkPool lane.
|
|
5
|
-
let input = ''
|
|
6
|
-
process.stdin.setEncoding('utf8')
|
|
7
|
-
process.stdin.on('data', (chunk) => { input += chunk })
|
|
8
|
-
process.stdin.on('end', () => {
|
|
9
|
-
let payload = null
|
|
10
|
-
try { payload = JSON.parse(input) } catch { /* malformed hook input fails closed below */ }
|
|
11
|
-
const blocking = !payload || payload.tool_name === 'delegate_task'
|
|
12
|
-
process.stdout.write(JSON.stringify(blocking
|
|
13
|
-
? { action: 'block', message: 'ThinkPool blocks hidden Hermes delegation. Open visible ThinkPool lanes instead.' }
|
|
14
|
-
: {}) + '\n')
|
|
15
|
-
})
|
|
2
|
+
let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",s=>{e+=s}),process.stdin.on("end",()=>{let s=null;try{s=JSON.parse(e)}catch{}const n=!s||"delegate_task"===s.tool_name;process.stdout.write(JSON.stringify(n?{action:"block",message:"ThinkPool blocks hidden Hermes delegation. Open visible ThinkPool lanes instead."}:{})+"\n")});
|
package/hermes-event-mapper.mjs
CHANGED
|
@@ -1,194 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const textOf = (content) => {
|
|
4
|
-
if (typeof content === 'string') return content
|
|
5
|
-
if (content?.type === 'text') return String(content.text || '')
|
|
6
|
-
if (content?.type === 'content') return textOf(content.content)
|
|
7
|
-
return ''
|
|
8
|
-
}
|
|
9
|
-
const outputText = (update) => {
|
|
10
|
-
if (typeof update?.rawOutput === 'string') return update.rawOutput
|
|
11
|
-
const text = (update?.content || []).map(textOf).filter(Boolean).join('\n')
|
|
12
|
-
if (text) return text
|
|
13
|
-
if (update?.rawOutput != null) {
|
|
14
|
-
try { return JSON.stringify(update.rawOutput) } catch { return String(update.rawOutput) }
|
|
15
|
-
}
|
|
16
|
-
return ''
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const diffInput = (content) => {
|
|
20
|
-
const diffs = (Array.isArray(content) ? content : []).filter((part) => part?.type === 'diff')
|
|
21
|
-
if (!diffs.length) return null
|
|
22
|
-
if (diffs.length === 1) return {
|
|
23
|
-
file_path: diffs[0].path || '', path: diffs[0].path || '',
|
|
24
|
-
old_string: diffs[0].oldText || '', new_string: diffs[0].newText || '',
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
file_path: diffs[0].path || '', path: diffs[0].path || '',
|
|
28
|
-
edits: diffs.map((diff) => ({ file_path: diff.path || '', old_string: diff.oldText || '', new_string: diff.newText || '' })),
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function hermesToolFor(update = {}) {
|
|
33
|
-
const kind = String(update.kind || '').toLowerCase()
|
|
34
|
-
const title = String(update.title || '')
|
|
35
|
-
const raw = update.rawInput && typeof update.rawInput === 'object' ? update.rawInput : {}
|
|
36
|
-
const server = String(update.server || raw.server || '')
|
|
37
|
-
const tool = String(update.tool || raw.tool || raw.name || '')
|
|
38
|
-
if (server === 'project-context' && tool === 'search_context_evidence') return { name: 'mcp__project-context__search_context_evidence', input: raw }
|
|
39
|
-
const terminal = (update.content || []).find((part) => part?.type === 'terminal')
|
|
40
|
-
const shellText = (update.content || []).map(textOf).find((text) => text.trim().startsWith('$ '))
|
|
41
|
-
const location = update.locations?.[0]?.path
|
|
42
|
-
if (kind === 'execute' || terminal || shellText) return { name: 'Bash', input: { ...raw, command: raw.command || shellText?.trim().slice(2) || title } }
|
|
43
|
-
if (kind === 'edit') {
|
|
44
|
-
const diff = diffInput(update.content)
|
|
45
|
-
const name = diff?.edits ? 'MultiEdit' : title.toLowerCase().includes('write') ? 'Write' : 'Edit'
|
|
46
|
-
return { name, input: { ...raw, ...(diff || {}), file_path: diff?.file_path || raw.path || location || '', path: diff?.path || raw.path || location || '' } }
|
|
47
|
-
}
|
|
48
|
-
if (kind === 'read') return { name: 'Read', input: { ...raw, file_path: raw.path || location || '', path: raw.path || location || '' } }
|
|
49
|
-
if (kind === 'search') return { name: 'Grep', input: raw }
|
|
50
|
-
if (kind === 'fetch') return { name: 'WebFetch', input: raw }
|
|
51
|
-
return { name: title || 'HermesTool', input: raw }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class HermesEventMapper {
|
|
55
|
-
constructor({ onEvent, sessionId = null, model = null } = {}) {
|
|
56
|
-
this.onEvent = onEvent
|
|
57
|
-
this.sessionId = sessionId
|
|
58
|
-
this.model = model
|
|
59
|
-
this.tools = new Map()
|
|
60
|
-
this.text = ''
|
|
61
|
-
this.thought = ''
|
|
62
|
-
this.textCid = null
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
_emit(event) { try { this.onEvent?.(event) } catch { /* consumer isolation */ } }
|
|
66
|
-
|
|
67
|
-
startSession({ sessionId, models, modes, commands = [] } = {}) {
|
|
68
|
-
this.sessionId = sessionId || this.sessionId
|
|
69
|
-
this.model = models?.currentModelId || this.model
|
|
70
|
-
const list = (models?.availableModels || []).map((item) => ({ value: item.modelId, displayName: item.name || item.modelId, description: item.description || '' }))
|
|
71
|
-
this._emit({ kind: 'system', sessionId: this.sessionId, model: this.model, commands, models: list, modes })
|
|
72
|
-
if (list.length) this._emit({ kind: 'models', models: list, model: this.model })
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
push(params = {}) {
|
|
76
|
-
if (params.sessionId && this.sessionId && params.sessionId !== this.sessionId) return
|
|
77
|
-
const update = params.update || params
|
|
78
|
-
switch (update.sessionUpdate) {
|
|
79
|
-
case 'agent_message_chunk': {
|
|
80
|
-
const chunk = textOf(update.content)
|
|
81
|
-
if (!chunk) return
|
|
82
|
-
this.text += chunk
|
|
83
|
-
this.textCid ||= `hermes-stream:${this.sessionId || 'session'}:${Date.now()}`
|
|
84
|
-
this._emit({ kind: 'assistant_stream', cid: this.textCid, text: this.text, streaming: true })
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
case 'agent_thought_chunk':
|
|
88
|
-
this.thought += textOf(update.content)
|
|
89
|
-
return
|
|
90
|
-
case 'tool_call': {
|
|
91
|
-
const tool = hermesToolFor(update)
|
|
92
|
-
this.tools.set(update.toolCallId, { ...update, ...tool, startedAt: Date.now() })
|
|
93
|
-
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: update.toolCallId, name: tool.name, input: tool.input }], parentToolUseId: null })
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
case 'tool_call_update': {
|
|
97
|
-
const prior = this.tools.get(update.toolCallId) || {}
|
|
98
|
-
const merged = { ...prior, ...update }
|
|
99
|
-
this.tools.set(update.toolCallId, merged)
|
|
100
|
-
if (!['completed', 'failed'].includes(update.status)) return
|
|
101
|
-
const completedTool = hermesToolFor(merged)
|
|
102
|
-
const content = [{ type: 'text', text: outputText(merged) }]
|
|
103
|
-
const evidence = evidenceForToolResult(completedTool.name, content)
|
|
104
|
-
this._emit({
|
|
105
|
-
kind: 'tool_result', toolUseId: update.toolCallId,
|
|
106
|
-
content, ...(evidence ? { evidence } : {}),
|
|
107
|
-
toolInput: { ...(prior.input || {}), ...(completedTool.input || {}) },
|
|
108
|
-
isError: update.status === 'failed',
|
|
109
|
-
durationMs: prior.startedAt ? Date.now() - prior.startedAt : undefined,
|
|
110
|
-
parentToolUseId: null,
|
|
111
|
-
})
|
|
112
|
-
this.tools.delete(update.toolCallId)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
case 'usage_update': {
|
|
116
|
-
const used = Number(update.used) || 0
|
|
117
|
-
const max = Number(update.size) || 0
|
|
118
|
-
this._emit({ kind: 'usage', ctx: { used, max, pct: max ? Math.min(100, Math.round((used / max) * 100)) : 0, over: max > 0 && used > max, model: this.model } })
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
case 'available_commands_update':
|
|
122
|
-
// ACP commands carry useful UI metadata. Keep strings accepted for
|
|
123
|
-
// older runtimes, but never flatten a native catalog on the way out.
|
|
124
|
-
this._emit({ kind: 'system', sessionId: this.sessionId, model: this.model, commands: (update.availableCommands || []).map((command) => {
|
|
125
|
-
if (typeof command === 'string') return command.startsWith('/') ? command : `/${command}`
|
|
126
|
-
const name = String(command?.name || '').replace(/^\/+/, '')
|
|
127
|
-
const hint = command?.input?.hint || command?.inputHint || command?.input_hint || ''
|
|
128
|
-
return {
|
|
129
|
-
name: `/${name}`,
|
|
130
|
-
description: String(command?.description || ''),
|
|
131
|
-
...(hint ? { inputHint: String(hint) } : {}),
|
|
132
|
-
}
|
|
133
|
-
}).filter((command) => typeof command === 'string' ? command !== '/' : command.name !== '/') })
|
|
134
|
-
return
|
|
135
|
-
case 'current_mode_update':
|
|
136
|
-
this._emit({ kind: 'mode', mode: update.currentModeId })
|
|
137
|
-
return
|
|
138
|
-
case 'plan': {
|
|
139
|
-
const todos = (update.entries || []).map((entry) => ({ content: entry.content || '', activeForm: entry.content || '', status: entry.status === 'in_progress' ? 'in_progress' : entry.status === 'completed' ? 'completed' : 'pending' }))
|
|
140
|
-
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: `hermes-plan:${this.sessionId}`, name: 'TodoWrite', input: { todos } }], parentToolUseId: null })
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
default:
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
finishTurn({ stopReason = 'end_turn', usage = null } = {}) {
|
|
149
|
-
for (const [toolCallId, tool] of this.tools) {
|
|
150
|
-
const cancelled = stopReason === 'cancelled'
|
|
151
|
-
this._emit({
|
|
152
|
-
kind: 'tool_result', toolUseId: toolCallId,
|
|
153
|
-
content: [{ type: 'text', text: cancelled ? '[cancelled]' : 'Hermes ended the turn without reporting this tool result.' }],
|
|
154
|
-
toolInput: tool.input || undefined,
|
|
155
|
-
isError: !cancelled && stopReason !== 'end_turn',
|
|
156
|
-
missing: !cancelled,
|
|
157
|
-
durationMs: tool.startedAt ? Date.now() - tool.startedAt : undefined,
|
|
158
|
-
parentToolUseId: null,
|
|
159
|
-
})
|
|
160
|
-
}
|
|
161
|
-
this.tools.clear()
|
|
162
|
-
const blocks = []
|
|
163
|
-
if (this.thought) blocks.push({ type: 'thinking', text: this.thought, reasoningKind: 'thought' })
|
|
164
|
-
if (this.text) blocks.push({ type: 'text', text: this.text })
|
|
165
|
-
if (blocks.length) this._emit({ kind: 'assistant', blocks, parentToolUseId: null, ...(this.textCid ? { replacesCid: this.textCid } : {}) })
|
|
166
|
-
this._emit({
|
|
167
|
-
kind: 'result', subtype: stopReason === 'cancelled' ? 'aborted' : stopReason === 'end_turn' ? 'success' : 'error_during_execution',
|
|
168
|
-
sessionId: this.sessionId, model: this.model, costUsd: null,
|
|
169
|
-
usage: usage ? {
|
|
170
|
-
input_tokens: Number(usage.inputTokens) || 0,
|
|
171
|
-
cache_read_input_tokens: Number(usage.cachedReadTokens) || 0,
|
|
172
|
-
cache_creation_input_tokens: 0,
|
|
173
|
-
output_tokens: (Number(usage.outputTokens) || 0) + (Number(usage.thoughtTokens) || 0),
|
|
174
|
-
} : null,
|
|
175
|
-
numTurns: 1, durationMs: undefined, denials: 0, resultText: this.text || null,
|
|
176
|
-
})
|
|
177
|
-
this.text = ''
|
|
178
|
-
this.thought = ''
|
|
179
|
-
this.textCid = null
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
compactOutcome() {
|
|
183
|
-
// /compact is handled locally by Hermes and its response is the only
|
|
184
|
-
// authoritative success signal. Do not infer a reset from a successful
|
|
185
|
-
// ACP envelope: no-op and failure also return end_turn.
|
|
186
|
-
const text = String(this.text || '')
|
|
187
|
-
const hit = text.match(/Context compressed:\s*(\d+)\s*->\s*(\d+)\s*messages[\s\S]*?~?([\d,]+)\s*->\s*~?([\d,]+)\s*tokens/i)
|
|
188
|
-
if (!hit) return null
|
|
189
|
-
return {
|
|
190
|
-
preMessages: Number(hit[1]), postMessages: Number(hit[2]),
|
|
191
|
-
preTokens: Number(hit[3].replace(/,/g, '')), postTokens: Number(hit[4].replace(/,/g, '')),
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
1
|
+
import{evidenceForToolResult as t}from"./evidence-citations.mjs";const e=t=>"string"==typeof t?t:"text"===t?.type?String(t.text||""):"content"===t?.type?e(t.content):"",s=t=>{if("string"==typeof t?.rawOutput)return t.rawOutput;const s=(t?.content||[]).map(e).filter(Boolean).join("\n");if(s)return s;if(null!=t?.rawOutput)try{return JSON.stringify(t.rawOutput)}catch{return String(t.rawOutput)}return""};export function hermesToolFor(t={}){const s=String(t.kind||"").toLowerCase(),n=String(t.title||""),o=t.rawInput&&"object"==typeof t.rawInput?t.rawInput:{},i=String(t.server||o.server||""),r=String(t.tool||o.tool||o.name||"");if("project-context"===i&&"search_context_evidence"===r)return{name:"mcp__project-context__search_context_evidence",input:o};const a=(t.content||[]).find(t=>"terminal"===t?.type),d=(t.content||[]).map(e).find(t=>t.trim().startsWith("$ ")),l=t.locations?.[0]?.path;if("execute"===s||a||d)return{name:"Bash",input:{...o,command:o.command||d?.trim().slice(2)||n}};if("edit"===s){const e=(t=>{const e=(Array.isArray(t)?t:[]).filter(t=>"diff"===t?.type);return e.length?1===e.length?{file_path:e[0].path||"",path:e[0].path||"",old_string:e[0].oldText||"",new_string:e[0].newText||""}:{file_path:e[0].path||"",path:e[0].path||"",edits:e.map(t=>({file_path:t.path||"",old_string:t.oldText||"",new_string:t.newText||""}))}:null})(t.content);return{name:e?.edits?"MultiEdit":n.toLowerCase().includes("write")?"Write":"Edit",input:{...o,...e||{},file_path:e?.file_path||o.path||l||"",path:e?.path||o.path||l||""}}}return"read"===s?{name:"Read",input:{...o,file_path:o.path||l||"",path:o.path||l||""}}:"search"===s?{name:"Grep",input:o}:"fetch"===s?{name:"WebFetch",input:o}:{name:n||"HermesTool",input:o}}export class HermesEventMapper{constructor({onEvent:t,sessionId:e=null,model:s=null}={}){this.onEvent=t,this.sessionId=e,this.model=s,this.tools=new Map,this.text="",this.thought="",this.textCid=null}_emit(t){try{this.onEvent?.(t)}catch{}}startSession({sessionId:t,models:e,modes:s,commands:n=[]}={}){this.sessionId=t||this.sessionId,this.model=e?.currentModelId||this.model;const o=(e?.availableModels||[]).map(t=>({value:t.modelId,displayName:t.name||t.modelId,description:t.description||""}));this._emit({kind:"system",sessionId:this.sessionId,model:this.model,commands:n,models:o,modes:s}),o.length&&this._emit({kind:"models",models:o,model:this.model})}push(n={}){if(n.sessionId&&this.sessionId&&n.sessionId!==this.sessionId)return;const o=n.update||n;switch(o.sessionUpdate){case"agent_message_chunk":{const t=e(o.content);if(!t)return;return this.text+=t,this.textCid||=`hermes-stream:${this.sessionId||"session"}:${Date.now()}`,void this._emit({kind:"assistant_stream",cid:this.textCid,text:this.text,streaming:!0})}case"agent_thought_chunk":return void(this.thought+=e(o.content));case"tool_call":{const t=hermesToolFor(o);return this.tools.set(o.toolCallId,{...o,...t,startedAt:Date.now()}),void this._emit({kind:"assistant",blocks:[{type:"tool_use",id:o.toolCallId,name:t.name,input:t.input}],parentToolUseId:null})}case"tool_call_update":{const e=this.tools.get(o.toolCallId)||{},n={...e,...o};if(this.tools.set(o.toolCallId,n),!["completed","failed"].includes(o.status))return;const i=hermesToolFor(n),r=[{type:"text",text:s(n)}],a=t(i.name,r);return this._emit({kind:"tool_result",toolUseId:o.toolCallId,content:r,...a?{evidence:a}:{},toolInput:{...e.input||{},...i.input||{}},isError:"failed"===o.status,durationMs:e.startedAt?Date.now()-e.startedAt:void 0,parentToolUseId:null}),void this.tools.delete(o.toolCallId)}case"usage_update":{const t=Number(o.used)||0,e=Number(o.size)||0;return void this._emit({kind:"usage",ctx:{used:t,max:e,pct:e?Math.min(100,Math.round(t/e*100)):0,over:e>0&&t>e,model:this.model}})}case"available_commands_update":return void this._emit({kind:"system",sessionId:this.sessionId,model:this.model,commands:(o.availableCommands||[]).map(t=>{if("string"==typeof t)return t.startsWith("/")?t:`/${t}`;const e=String(t?.name||"").replace(/^\/+/,""),s=t?.input?.hint||t?.inputHint||t?.input_hint||"";return{name:`/${e}`,description:String(t?.description||""),...s?{inputHint:String(s)}:{}}}).filter(t=>"string"==typeof t?"/"!==t:"/"!==t.name)});case"current_mode_update":return void this._emit({kind:"mode",mode:o.currentModeId});case"plan":{const t=(o.entries||[]).map(t=>({content:t.content||"",activeForm:t.content||"",status:"in_progress"===t.status?"in_progress":"completed"===t.status?"completed":"pending"}));return void this._emit({kind:"assistant",blocks:[{type:"tool_use",id:`hermes-plan:${this.sessionId}`,name:"TodoWrite",input:{todos:t}}],parentToolUseId:null})}default:return}}finishTurn({stopReason:t="end_turn",usage:e=null}={}){for(const[e,s]of this.tools){const n="cancelled"===t;this._emit({kind:"tool_result",toolUseId:e,content:[{type:"text",text:n?"[cancelled]":"Hermes ended the turn without reporting this tool result."}],toolInput:s.input||void 0,isError:!n&&"end_turn"!==t,missing:!n,durationMs:s.startedAt?Date.now()-s.startedAt:void 0,parentToolUseId:null})}this.tools.clear();const s=[];this.thought&&s.push({type:"thinking",text:this.thought,reasoningKind:"thought"}),this.text&&s.push({type:"text",text:this.text}),s.length&&this._emit({kind:"assistant",blocks:s,parentToolUseId:null,...this.textCid?{replacesCid:this.textCid}:{}}),this._emit({kind:"result",subtype:"cancelled"===t?"aborted":"end_turn"===t?"success":"error_during_execution",sessionId:this.sessionId,model:this.model,costUsd:null,usage:e?{input_tokens:Number(e.inputTokens)||0,cache_read_input_tokens:Number(e.cachedReadTokens)||0,cache_creation_input_tokens:0,output_tokens:(Number(e.outputTokens)||0)+(Number(e.thoughtTokens)||0)}:null,numTurns:1,durationMs:void 0,denials:0,resultText:this.text||null}),this.text="",this.thought="",this.textCid=null}compactOutcome(){const t=String(this.text||"").match(/Context compressed:\s*(\d+)\s*->\s*(\d+)\s*messages[\s\S]*?~?([\d,]+)\s*->\s*~?([\d,]+)\s*tokens/i);return t?{preMessages:Number(t[1]),postMessages:Number(t[2]),preTokens:Number(t[3].replace(/,/g,"")),postTokens:Number(t[4].replace(/,/g,""))}:null}}
|
package/hermes-isolation.mjs
CHANGED
|
@@ -1,61 +1 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { randomUUID } from 'node:crypto'
|
|
5
|
-
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
|
|
6
|
-
import { startHermesSession } from './hermes-session.mjs'
|
|
7
|
-
import { withMcpSessionFactory } from './codex-mcp-http.mjs'
|
|
8
|
-
|
|
9
|
-
const waitFor = (predicate, timeoutMs = 25_000) => new Promise((resolve, reject) => {
|
|
10
|
-
const started = Date.now()
|
|
11
|
-
const timer = setInterval(() => {
|
|
12
|
-
if (predicate()) { clearInterval(timer); resolve() }
|
|
13
|
-
else if (Date.now() - started > timeoutMs) { clearInterval(timer); reject(new Error('Hermes MCP isolation probe timed out')) }
|
|
14
|
-
}, 50)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
export function assertHermesIsolation(outputs, markers) {
|
|
18
|
-
if (!Array.isArray(outputs) || outputs.length !== 2 || !Array.isArray(markers) || markers.length !== 2) {
|
|
19
|
-
throw new TypeError('Hermes MCP isolation requires exactly two session outputs and markers')
|
|
20
|
-
}
|
|
21
|
-
for (let i = 0; i < 2; i++) {
|
|
22
|
-
if (!outputs[i].includes(`mcp__thinkpool__${markers[i]}`)) throw new Error(`Hermes lane ${i + 1} did not advertise its own MCP marker`)
|
|
23
|
-
if (outputs[i].includes(`mcp__thinkpool__${markers[1 - i]}`)) throw new Error(`Hermes lane ${i + 1} advertised the other lane's MCP marker`)
|
|
24
|
-
}
|
|
25
|
-
return true
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function verifyHermesMcpIsolation({ rounds = 2, sessionFactory = startHermesSession } = {}) {
|
|
29
|
-
const evidence = []
|
|
30
|
-
for (let round = 0; round < rounds; round++) {
|
|
31
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tp-hermes-mcp-isolation-'))
|
|
32
|
-
const suffix = randomUUID().replaceAll('-', '').slice(0, 10)
|
|
33
|
-
const markers = [`lane_alpha_${suffix}`, `lane_beta_${suffix}`]
|
|
34
|
-
const events = [[], []]
|
|
35
|
-
const sessions = []
|
|
36
|
-
try {
|
|
37
|
-
for (let i = 0; i < 2; i++) {
|
|
38
|
-
const cwd = path.join(root, i === 0 ? 'alpha' : 'beta')
|
|
39
|
-
fs.mkdirSync(cwd)
|
|
40
|
-
const createMarkerServer = () => createSdkMcpServer({
|
|
41
|
-
name: 'thinkpool', version: '1.0.0',
|
|
42
|
-
tools: [tool(markers[i], 'Synthetic ThinkPool lane-isolation marker.', {}, async () => ({ content: [{ type: 'text', text: markers[i] }] }))],
|
|
43
|
-
})
|
|
44
|
-
const sdkServer = withMcpSessionFactory(createMarkerServer)
|
|
45
|
-
sessions.push(sessionFactory({ cwd, lazy: true, mcpServers: { thinkpool: sdkServer }, onEvent: (event) => events[i].push(event) }))
|
|
46
|
-
}
|
|
47
|
-
sessions.forEach((session) => session.sendTurn('/tools'))
|
|
48
|
-
await waitFor(() => events.every((list) => list.some((event) => event.kind === 'result') || list.some((event) => event.kind === 'error')))
|
|
49
|
-
const errors = events.flatMap((list) => list.filter((event) => event.kind === 'error'))
|
|
50
|
-
if (errors.length) throw new Error(`Hermes MCP isolation probe failed: ${errors.map((event) => event.message).join('; ')}`)
|
|
51
|
-
const outputs = events.map((list) => list.filter((event) => event.kind === 'assistant_stream').map((event) => event.text).join(''))
|
|
52
|
-
assertHermesIsolation(outputs, markers)
|
|
53
|
-
evidence.push({ round: round + 1, markers, sessionIds: sessions.map((session) => session.sessionId) })
|
|
54
|
-
} finally {
|
|
55
|
-
sessions.forEach((session) => session.end())
|
|
56
|
-
await new Promise((resolve) => setTimeout(resolve, 250))
|
|
57
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return evidence
|
|
61
|
-
}
|
|
1
|
+
import e from"node:fs";import r from"node:os";import o from"node:path";import{randomUUID as t}from"node:crypto";import{createSdkMcpServer as n,tool as s}from"@anthropic-ai/claude-agent-sdk";import{startHermesSession as i}from"./hermes-session.mjs";import{withMcpSessionFactory as a}from"./codex-mcp-http.mjs";const m=(e,r=25e3)=>new Promise((o,t)=>{const n=Date.now(),s=setInterval(()=>{e()?(clearInterval(s),o()):Date.now()-n>r&&(clearInterval(s),t(new Error("Hermes MCP isolation probe timed out")))},50)});export function assertHermesIsolation(e,r){if(!Array.isArray(e)||2!==e.length||!Array.isArray(r)||2!==r.length)throw new TypeError("Hermes MCP isolation requires exactly two session outputs and markers");for(let o=0;o<2;o++){if(!e[o].includes(`mcp__thinkpool__${r[o]}`))throw new Error(`Hermes lane ${o+1} did not advertise its own MCP marker`);if(e[o].includes(`mcp__thinkpool__${r[1-o]}`))throw new Error(`Hermes lane ${o+1} advertised the other lane's MCP marker`)}return!0}export async function verifyHermesMcpIsolation({rounds:l=2,sessionFactory:p=i}={}){const c=[];for(let i=0;i<l;i++){const l=e.mkdtempSync(o.join(r.tmpdir(),"tp-hermes-mcp-isolation-")),d=t().replaceAll("-","").slice(0,10),h=[`lane_alpha_${d}`,`lane_beta_${d}`],f=[[],[]],u=[];try{for(let r=0;r<2;r++){const t=o.join(l,0===r?"alpha":"beta");e.mkdirSync(t);const i=a(()=>n({name:"thinkpool",version:"1.0.0",tools:[s(h[r],"Synthetic ThinkPool lane-isolation marker.",{},async()=>({content:[{type:"text",text:h[r]}]}))]}));u.push(p({cwd:t,lazy:!0,mcpServers:{thinkpool:i},onEvent:e=>f[r].push(e)}))}u.forEach(e=>e.sendTurn("/tools")),await m(()=>f.every(e=>e.some(e=>"result"===e.kind)||e.some(e=>"error"===e.kind)));const r=f.flatMap(e=>e.filter(e=>"error"===e.kind));if(r.length)throw new Error(`Hermes MCP isolation probe failed: ${r.map(e=>e.message).join("; ")}`);assertHermesIsolation(f.map(e=>e.filter(e=>"assistant_stream"===e.kind).map(e=>e.text).join("")),h),c.push({round:i+1,markers:h,sessionIds:u.map(e=>e.sessionId)})}finally{u.forEach(e=>e.end()),await new Promise(e=>setTimeout(e,250)),e.rmSync(l,{recursive:!0,force:!0})}}return c}
|
package/hermes-model-cache.mjs
CHANGED
|
@@ -1,54 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// to 30 seconds while the ACP process boots. The catalog is public metadata and
|
|
3
|
-
// identical for every room served by this Hermes profile, so keep the last
|
|
4
|
-
// validated copy at the host level. A live Hermes session always replaces it.
|
|
5
|
-
|
|
6
|
-
import fs from 'node:fs'
|
|
7
|
-
import os from 'node:os'
|
|
8
|
-
import path from 'node:path'
|
|
9
|
-
|
|
10
|
-
const MAX_MODELS = 1000
|
|
11
|
-
const MAX_BYTES = 512 * 1024
|
|
12
|
-
const MAX_FIELD = 512
|
|
13
|
-
|
|
14
|
-
const cachePath = () => process.env.TP_HERMES_MODEL_CACHE
|
|
15
|
-
|| path.join(process.env.TP_PAIR_ROOT || path.join(os.homedir(), '.thinkpool-pair'), 'hermes-models.json')
|
|
16
|
-
|
|
17
|
-
export function normalizeHermesModelCache(models) {
|
|
18
|
-
if (!Array.isArray(models)) return []
|
|
19
|
-
const seen = new Set()
|
|
20
|
-
const normalized = []
|
|
21
|
-
for (const raw of models.slice(0, MAX_MODELS)) {
|
|
22
|
-
const value = String(raw?.value || '').trim().slice(0, MAX_FIELD)
|
|
23
|
-
if (!value || seen.has(value)) continue
|
|
24
|
-
seen.add(value)
|
|
25
|
-
normalized.push({
|
|
26
|
-
value,
|
|
27
|
-
displayName: String(raw?.displayName || value).trim().slice(0, MAX_FIELD) || value,
|
|
28
|
-
description: String(raw?.description || '').trim().slice(0, MAX_FIELD),
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
return normalized
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function loadHermesModelCache() {
|
|
35
|
-
try {
|
|
36
|
-
const file = cachePath()
|
|
37
|
-
if ((Number(fs.statSync(file).size) || 0) > MAX_BYTES) return []
|
|
38
|
-
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
39
|
-
return normalizeHermesModelCache(parsed?.models)
|
|
40
|
-
} catch { return [] }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function saveHermesModelCache(models) {
|
|
44
|
-
const normalized = normalizeHermesModelCache(models)
|
|
45
|
-
if (!normalized.length) return []
|
|
46
|
-
try {
|
|
47
|
-
const file = cachePath()
|
|
48
|
-
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
49
|
-
const temp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
50
|
-
fs.writeFileSync(temp, JSON.stringify({ savedAt: Date.now(), models: normalized }), { mode: 0o600 })
|
|
51
|
-
fs.renameSync(temp, file)
|
|
52
|
-
} catch { /* cache is opportunistic; live discovery remains the fallback */ }
|
|
53
|
-
return normalized
|
|
54
|
-
}
|
|
1
|
+
import e from"node:fs";import r from"node:os";import o from"node:path";const n=()=>process.env.TP_HERMES_MODEL_CACHE||o.join(process.env.TP_PAIR_ROOT||o.join(r.homedir(),".thinkpool-pair"),"hermes-models.json");export function normalizeHermesModelCache(e){if(!Array.isArray(e))return[];const r=new Set,o=[];for(const n of e.slice(0,1e3)){const e=String(n?.value||"").trim().slice(0,512);e&&!r.has(e)&&(r.add(e),o.push({value:e,displayName:String(n?.displayName||e).trim().slice(0,512)||e,description:String(n?.description||"").trim().slice(0,512)}))}return o}export function loadHermesModelCache(){try{const r=n();if((Number(e.statSync(r).size)||0)>524288)return[];const o=JSON.parse(e.readFileSync(r,"utf8"));return normalizeHermesModelCache(o?.models)}catch{return[]}}export function saveHermesModelCache(r){const t=normalizeHermesModelCache(r);if(!t.length)return[];try{const r=n();e.mkdirSync(o.dirname(r),{recursive:!0});const s=`${r}.${process.pid}.${Date.now()}.tmp`;e.writeFileSync(s,JSON.stringify({savedAt:Date.now(),models:t}),{mode:384}),e.renameSync(s,r)}catch{}return t}
|