thinkpool-pair 0.7.333 → 0.7.335
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/bridge.mjs +4 -4
- package/package.json +2 -1
- package/review-check.mjs +19 -7
- package/terminal-name.mjs +74 -19
- package/thinkpool-capabilities.json +3 -3
package/bridge.mjs
CHANGED
|
@@ -92,7 +92,7 @@ function stopFlowPreviews (flowId, laneId = null) {
|
|
|
92
92
|
}
|
|
93
93
|
import { FLOW_REVIEWER_PROMPT, FLOW_CODEX_REVIEWER_PROMPT, revertLane, parseReviewVerdict, reviewVerdictToReflection } from './flow-review.mjs'
|
|
94
94
|
import { reviewGateDecision } from './flow-review-gate.mjs'
|
|
95
|
-
import {
|
|
95
|
+
import { IMMUTABLE_REVIEW_READ_TOOL_EXTRAS, UNSANDBOXED_REVIEW_CHECK_TOOL_EXTRAS, readReviewFile, runReviewCheck } from './review-check.mjs'
|
|
96
96
|
import { pairAdjudicationPrompt, reviewReflectionDecision, REVIEW_DEFAULTS } from './flow-review-reflect.mjs'
|
|
97
97
|
import { mergeWorktrees, inlineSingleHtml, initRepo } from './flow-assembly.mjs'
|
|
98
98
|
import { canDispatch, FLOW_LIMITS, makeBudget, recordSpend, killSwitchEnv } from './flow-budget.mjs'
|
|
@@ -3032,13 +3032,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
3032
3032
|
'Read one regular file from the declared immutable review target. This always reads the target’s pinned Git object, never a live worktree; target and path are validated and the response is byte-bounded.',
|
|
3033
3033
|
{ target: z.string(), path: z.string() },
|
|
3034
3034
|
async (args) => ({ content: [{ type: 'text', text: JSON.stringify(await readReviewFile({ target: args?.target, filePath: args?.path, snapshots: entry.flowReviewSnapshots })) }] }),
|
|
3035
|
-
|
|
3035
|
+
IMMUTABLE_REVIEW_READ_TOOL_EXTRAS,
|
|
3036
3036
|
), tool(
|
|
3037
3037
|
'run_review_check',
|
|
3038
|
-
'Run one fixed verification check against an immutable git-archived review target. This accepts only the declared target and a fixed check id
|
|
3038
|
+
'Run one fixed verification check against an immutable git-archived review target. This accepts only the declared target and a fixed check id, but executes target-defined code in a scratch archive that is not an OS filesystem/network sandbox. It can require approval and must be refused by approvalPolicy=never reviewers.',
|
|
3039
3039
|
{ target: z.string(), checkId: z.string() },
|
|
3040
3040
|
async (args) => ({ content: [{ type: 'text', text: JSON.stringify(await runReviewCheck({ target: args?.target, checkId: args?.checkId, snapshots: entry.flowReviewSnapshots })) }] }),
|
|
3041
|
-
|
|
3041
|
+
UNSANDBOXED_REVIEW_CHECK_TOOL_EXTRAS,
|
|
3042
3042
|
)] : []),
|
|
3043
3043
|
],
|
|
3044
3044
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.335",
|
|
4
4
|
"description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"@anthropic-ai/claude-agent-sdk": "^0.3.198",
|
|
114
114
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
115
115
|
"@supabase/supabase-js": "^2.45.0",
|
|
116
|
+
"compromise": "14.16.0",
|
|
116
117
|
"yaml": "2.9.0",
|
|
117
118
|
"zod": "^4.4.3"
|
|
118
119
|
},
|
package/review-check.mjs
CHANGED
|
@@ -17,13 +17,10 @@ const CHECKS = Object.freeze({
|
|
|
17
17
|
})
|
|
18
18
|
const OUTPUT_CAP = 512 * 1024
|
|
19
19
|
|
|
20
|
-
// Codex's native read-only/review mode never raises approval cards.
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
// for both source reads and fixed scratch checks: neither mutates the reviewed
|
|
25
|
-
// target, neither reaches the open world, and repeating either is safe.
|
|
26
|
-
export const IMMUTABLE_REVIEW_TOOL_EXTRAS = Object.freeze({
|
|
20
|
+
// Codex's native read-only/review mode never raises approval cards. Mark only
|
|
21
|
+
// the pinned Git-blob reader as approval-free: it cannot execute target code,
|
|
22
|
+
// mutate the target, or reach the open world.
|
|
23
|
+
export const IMMUTABLE_REVIEW_READ_TOOL_EXTRAS = Object.freeze({
|
|
27
24
|
annotations: Object.freeze({
|
|
28
25
|
readOnlyHint: true,
|
|
29
26
|
destructiveHint: false,
|
|
@@ -32,6 +29,21 @@ export const IMMUTABLE_REVIEW_TOOL_EXTRAS = Object.freeze({
|
|
|
32
29
|
}),
|
|
33
30
|
})
|
|
34
31
|
|
|
32
|
+
// Fixed check ids prevent command injection, but the selected check still runs
|
|
33
|
+
// target-defined code. The scratch archive protects the reviewed worktree; it
|
|
34
|
+
// is not an OS filesystem/network sandbox. Advertise that boundary honestly so
|
|
35
|
+
// approvalPolicy=never reviewers refuse execution instead of silently granting
|
|
36
|
+
// host/open-world authority. Sandboxed runners can still expose this behind an
|
|
37
|
+
// explicit approval in runtimes that support one.
|
|
38
|
+
export const UNSANDBOXED_REVIEW_CHECK_TOOL_EXTRAS = Object.freeze({
|
|
39
|
+
annotations: Object.freeze({
|
|
40
|
+
readOnlyHint: false,
|
|
41
|
+
destructiveHint: true,
|
|
42
|
+
idempotentHint: false,
|
|
43
|
+
openWorldHint: true,
|
|
44
|
+
}),
|
|
45
|
+
})
|
|
46
|
+
|
|
35
47
|
export function reviewCheckCommand(checkId) {
|
|
36
48
|
if (!Object.hasOwn(CHECKS, checkId)) throw new Error('unapproved review check')
|
|
37
49
|
const [command, args] = CHECKS[checkId]
|
package/terminal-name.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { nativeClaudeProviderAccessFailed } from './cross-terminal.mjs'
|
|
2
|
+
import nlp from 'compromise/two'
|
|
2
3
|
|
|
3
4
|
const SKIP = new Set([
|
|
4
5
|
'a', 'an', 'and', 'are', 'at', 'be', 'can', 'could', 'for', 'from', 'how', 'i',
|
|
@@ -18,12 +19,66 @@ const NOISE = /^(?:context|background|for reference|here(?:'s| is)|note|current(
|
|
|
18
19
|
const EXPLANATION = /^(?:because|cause|since|so that|this is because)\b/i
|
|
19
20
|
const CONSTRAINT = /^(?:users? can still|keep|must|never|should|without)\b/i
|
|
20
21
|
const DEPENDENT_FRAGMENT = /^(?:after|before|by|during|for|from|in|instead(?: of)?|on|through|until|with|without)\b/i
|
|
21
|
-
const INFORMATION_REQUEST = /^(?:which|what|who|where|when|why|how)\b/i
|
|
22
|
-
const NEGATIVE_PREFERENCE = /^(?:(?:i|we)\s+)?(?:do not|don't|dont|would not|wouldn't|won't|wont)\s+(?:(?:want|wanna|need)(?:\s+to)?|use|include|choose)\b|^(?:(?:i|we)\s+)?(?:want|wanna|need)(?:\s+to)?\s+avoid\b/i
|
|
23
22
|
const ISSUE = /\b(?:broken|buggy|crash(?:es|ed|ing)?|duplicate|error|fail(?:s|ed|ing|ure)?|flash(?:es|ed|ing)?|missing|no animation|not working|out of (?:scrollable )?view|stuck|wrong)\b/i
|
|
24
23
|
const REQUEST = /^(?:please\s+)?(?:can|could|would|will)\s+(?:we|you)\b|^(?:please\s+)?(?:how about|let's|let us|we need to|i want you to)\b/i
|
|
25
24
|
const SECRET = /(?<![\p{L}\p{N}])(?:sk-(?:proj-)?[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|xox[baprs]-[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9_-]{8,}|glpat-[a-z0-9_-]{8,}|npm_[a-z0-9]{24,}|(?:sk|rk)_(?:live|test)_[a-z0-9]{8,}|whsec_[a-z0-9]{8,}|AIza[a-z0-9_-]{8,}|AKIA[A-Z0-9]{12,}|bearer\s+[a-z0-9._-]{8,}|eyJ[a-z0-9_-]{8,}\.[a-z0-9_-]{8,}\.[a-z0-9_-]{8,})(?![\p{L}\p{N}])/giu
|
|
26
25
|
const HANDOFF_MARKER = /^---\s*(?:the person's next message|current person request(?:\s*\([^\n]*\))?|side task)\s*---\s*$/gim
|
|
26
|
+
const QUESTION_WORDS = new Set(['how', 'what', 'when', 'where', 'which', 'who', 'whom', 'whose', 'why'])
|
|
27
|
+
const PREFERENCE_WORDS = new Set(['avoid', 'choose', 'include', 'need', 'use', 'want'])
|
|
28
|
+
const PREFERENCE_OBJECT_WORDS = new Set(['avoid', 'choose', 'include', 'use'])
|
|
29
|
+
|
|
30
|
+
const conceptsForTerm = (term) => new Set(
|
|
31
|
+
[term?.text, term?.normal, term?.machine, term?.implicit]
|
|
32
|
+
.map((value) => String(value || '').trim().toLowerCase())
|
|
33
|
+
.filter(Boolean),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const analyzeGrammar = (value, suppliedTerms = null) => {
|
|
37
|
+
const text = String(value || '').trim()
|
|
38
|
+
const terms = suppliedTerms || nlp(text).json({ terms: true }).flatMap((sentence) => sentence.terms || [])
|
|
39
|
+
const visible = terms
|
|
40
|
+
.filter((term) => String(term?.text || '').trim())
|
|
41
|
+
.map((term) => ({ text: term.text, concepts: conceptsForTerm(term), tags: new Set(term.tags || []) }))
|
|
42
|
+
const hasQuestionWord = visible.some((term) => [...term.concepts].some((word) => QUESTION_WORDS.has(word)))
|
|
43
|
+
const taggedQuestionWord = visible.some((term) => term.tags.has('QuestionWord'))
|
|
44
|
+
const negative = terms.some((term) => Array.isArray(term?.tags) && term.tags.includes('Negative'))
|
|
45
|
+
const preference = terms.some((term) => [...conceptsForTerm(term)].some((word) => PREFERENCE_WORDS.has(word)))
|
|
46
|
+
return {
|
|
47
|
+
text,
|
|
48
|
+
visible,
|
|
49
|
+
informationRequest: hasQuestionWord && (taggedQuestionWord || /\?\s*$/.test(text)),
|
|
50
|
+
negativePreference: negative && preference,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parsedClauses = (value) => String(value || '')
|
|
55
|
+
.replace(/,\s*(?:but|however)\s+/gi, '\n')
|
|
56
|
+
.split(/\n+|;\s*/)
|
|
57
|
+
.flatMap((part) => {
|
|
58
|
+
const text = part.trim()
|
|
59
|
+
if (!text) return []
|
|
60
|
+
const sentences = nlp(text).json({ terms: true })
|
|
61
|
+
return sentences.length
|
|
62
|
+
? sentences.map((sentence) => analyzeGrammar(sentence.text, sentence.terms || []))
|
|
63
|
+
: [analyzeGrammar(text)]
|
|
64
|
+
})
|
|
65
|
+
.filter((clause) => clause.text)
|
|
66
|
+
|
|
67
|
+
const questionSubject = (analysis) => {
|
|
68
|
+
const index = analysis.visible.findIndex((term) => [...term.concepts].some((word) => QUESTION_WORDS.has(word)))
|
|
69
|
+
return index >= 0 ? analysis.visible.slice(index + 1).map((term) => term.text).join(' ') : analysis.text
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const negativePreferenceSubject = (analysis) => {
|
|
73
|
+
let index = -1
|
|
74
|
+
for (let i = 0; i < analysis.visible.length; i++) {
|
|
75
|
+
if ([...analysis.visible[i].concepts].some((word) => PREFERENCE_OBJECT_WORDS.has(word))) index = i
|
|
76
|
+
}
|
|
77
|
+
if (index < 0) {
|
|
78
|
+
index = analysis.visible.findIndex((term) => [...term.concepts].some((word) => word === 'need' || word === 'want'))
|
|
79
|
+
}
|
|
80
|
+
return index >= 0 ? analysis.visible.slice(index + 1).map((term) => term.text).join(' ') : analysis.text
|
|
81
|
+
}
|
|
27
82
|
|
|
28
83
|
const ACTIONS = Object.freeze([
|
|
29
84
|
{ title: 'Fix', score: 100, re: /\b(?:fix|fixes|fixed|fixing|repair|repairs|repaired|repairing)\b/i },
|
|
@@ -117,35 +172,31 @@ const explicitIntent = (text) => {
|
|
|
117
172
|
}
|
|
118
173
|
|
|
119
174
|
const bestIntentClause = (text) => {
|
|
120
|
-
const clauses = text
|
|
121
|
-
.replace(/,\s*(?:but|however)\s+/gi, '\n')
|
|
122
|
-
.split(/\n+|(?<=[.!?;])\s+/)
|
|
123
|
-
.map((part) => part.trim())
|
|
124
|
-
.filter(Boolean)
|
|
175
|
+
const clauses = parsedClauses(text)
|
|
125
176
|
let best = null
|
|
126
|
-
for (
|
|
127
|
-
const clause =
|
|
177
|
+
for (const parsed of clauses) {
|
|
178
|
+
const clause = parsed.text.replace(/^[-*#>\d.)\s]+/, '').trim()
|
|
128
179
|
if (!clause || /^(?:https?:\/\/|\/|[A-Za-z]:\\)/.test(clause)) continue
|
|
129
|
-
const
|
|
180
|
+
const grammar = clause === parsed.text ? parsed : analyzeGrammar(clause)
|
|
130
181
|
const action = bestAction(clause)
|
|
131
182
|
let score = action?.score || 0
|
|
132
183
|
if (REQUEST.test(clause)) score += 18
|
|
133
|
-
if (
|
|
184
|
+
if (grammar.informationRequest) score += 36
|
|
134
185
|
if (ISSUE.test(clause)) score += /\b(?:crash|fail|flash|stuck|wrong)\w*\b/i.test(clause) ? 36 : 26
|
|
135
|
-
if (
|
|
136
|
-
else if (
|
|
186
|
+
if (grammar.visible.length >= 3 && grammar.visible.length <= 24) score += 10
|
|
187
|
+
else if (grammar.visible.length > 40) score -= 14
|
|
137
188
|
if (NOISE.test(clause)) score -= 30
|
|
138
189
|
if (EXPLANATION.test(clause)) score -= 34
|
|
139
190
|
if (CONSTRAINT.test(clause)) score -= 45
|
|
140
|
-
if (
|
|
191
|
+
if (grammar.negativePreference) score -= 70
|
|
141
192
|
// A short dependent tail such as "From ground up" or "On mobile" adds
|
|
142
193
|
// scope to the preceding request; it is not a standalone task. Recency is
|
|
143
194
|
// deliberately not a signal here: the title should represent the main
|
|
144
195
|
// theme, not whichever sentence happened to come last.
|
|
145
|
-
if (!action && !REQUEST.test(clause) && !
|
|
196
|
+
if (!action && !REQUEST.test(clause) && !grammar.informationRequest && DEPENDENT_FRAGMENT.test(clause)) score -= 24
|
|
146
197
|
if (!best || score > best.score) best = { clause, score }
|
|
147
198
|
}
|
|
148
|
-
return best?.clause || clauses[0] || null
|
|
199
|
+
return best?.clause || clauses[0]?.text || null
|
|
149
200
|
}
|
|
150
201
|
|
|
151
202
|
export function extractTerminalTask(value) {
|
|
@@ -210,9 +261,13 @@ const taskTitle = (value) => {
|
|
|
210
261
|
.replace(/^\s*(?:how about|i (?:do not|don't) know|i guess|i want you to|we need to|your task is to|let(?:'s| us))\s+/i, '')
|
|
211
262
|
.trim()
|
|
212
263
|
if (!body) return null
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
264
|
+
const grammar = analyzeGrammar(body)
|
|
265
|
+
if (grammar.negativePreference) {
|
|
266
|
+
const subject = contentWords(negativePreferenceSubject(grammar)).slice(0, 4)
|
|
267
|
+
return subject.length ? cleanTerminalName(['Avoid', ...subject.map(titleWord)].join(' ')) : null
|
|
268
|
+
}
|
|
269
|
+
const informationRequest = grammar.informationRequest
|
|
270
|
+
if (informationRequest) body = questionSubject(grammar).trim()
|
|
216
271
|
const signalText = body
|
|
217
272
|
body = body
|
|
218
273
|
.replace(/\s*,?\s+(?:so (?:that|you|we|it)\b|because\b|cause\b|if you know what i mean\b)[\s\S]*$/i, '')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bundleVersion":
|
|
3
|
+
"bundleVersion": 16,
|
|
4
4
|
"contracts": [
|
|
5
5
|
{
|
|
6
6
|
"id": "room-coordination",
|
|
@@ -130,13 +130,13 @@
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"id": "flow-completion",
|
|
133
|
-
"version":
|
|
133
|
+
"version": 2,
|
|
134
134
|
"routes": [
|
|
135
135
|
{
|
|
136
136
|
"id": "flow-completion",
|
|
137
137
|
"tools": ["submit_flow_plan", "mark_flow_done", "submit_flow_review", "read_review_file", "run_review_check"],
|
|
138
138
|
"trigger": "\\b(flow|submit_flow_plan|mark_flow_done|submit_flow_review|read_review_file|run_review_check)\\b",
|
|
139
|
-
"prompt": "Managed Flow roles use only their exposed completion and
|
|
139
|
+
"prompt": "Managed Flow roles use only their exposed completion and review tools: submit_flow_plan, mark_flow_done, submit_flow_review, read_review_file, and run_review_check. read_review_file is approval-free pinned-source access. run_review_check executes target-defined code without OS filesystem or network isolation and therefore remains subject to the runtime's normal approval boundary; approvalPolicy=never reviewers must not call it and must report the skipped check."
|
|
140
140
|
}
|
|
141
141
|
],
|
|
142
142
|
"impact": [
|