@dsh-fly/dsh-plugin-fly-mode 0.1.4 → 0.1.6
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/cordis.patch.yml +28 -5
- package/package.json +2 -2
- package/tools/dsh-plugin-fly-artifact/index.js +931 -0
- package/tools/dsh-plugin-fly-artifact/package.json +23 -0
- package/tools/dsh-plugin-fly-binary/index.js +12 -3
- package/tools/dsh-plugin-fly-blackbox/index.js +429 -3
- package/tools/dsh-plugin-fly-diag/index.js +1 -1
- package/tools/dsh-plugin-fly-recon/index.js +1 -1
- package/tools/dsh-plugin-fly-reverse/index.js +2 -2
- package/tools/dsh-plugin-fly-review/README.md +47 -0
- package/tools/dsh-plugin-fly-review/package.json +37 -0
- package/tools/dsh-plugin-fly-review/tools/index.js +328 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-plugin-fly-review` —— **评审姿态**(agent 平面插件,照 `@deepseek-ai/dsh-plan-mode` 的形状 ✓)。
|
|
3
|
+
*
|
|
4
|
+
* 它做的事只有一件:当会话处在评审姿态时,给**每次模型请求**插一段引导(`review:policy` 段),
|
|
5
|
+
* 告诉模型"只读地看、先讲阻塞问题、给 path:line 证据、不夸不复述";收口工具 `exit_review_mode`
|
|
6
|
+
* 把判词交用户裁决,用户接受即退出姿态。
|
|
7
|
+
*
|
|
8
|
+
* 三条来自上游的实测事实(决定了本文件的写法):
|
|
9
|
+
* · 姿态的**效果全部**来自那段引导 —— 进/出姿态只改提示词段,**不改工具目录**(收口工具常驻注册);
|
|
10
|
+
* · 状态记在**会话日志**里(`session.append('review/mode', {active})`),由会话投影折叠
|
|
11
|
+
* ⇒ resume / fork 自动恢复;切态落在**下一个被接受的 turn 边界**(`agent/pre-step`);
|
|
12
|
+
* · **权限不归它管**:沙箱与审批策略独立执行 ⇒ 这里的"只读"是**引导**,不是强制(README 写明 ✓,
|
|
13
|
+
* 不许在文案里说成强制 ✗)。
|
|
14
|
+
*
|
|
15
|
+
* 纪律文本取自 smart 的 `consultPrompt`(`dsh-plugin-fly-smart/tools/index.js:352-365`,2026-10-13 版)。
|
|
16
|
+
* 用户裁决"**不动 smart 自己**" ⇒ 两份文本**共存**(本文件一份、smart 一份);要收敛时走
|
|
17
|
+
* "抽库 + 两个消费者构建期内联"那条老路(同 `@dsh-fly/fly-panel-style` 的姿势),不在这里做跨仓钉死。
|
|
18
|
+
*/
|
|
19
|
+
import { Service } from '@deepseek-ai/cordis'
|
|
20
|
+
import { brandString } from '@deepseek-ai/dsh-brand'
|
|
21
|
+
import { z } from 'zod'
|
|
22
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
23
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
24
|
+
import { UserQuestionError } from '@deepseek-ai/dsh-user-questions'
|
|
25
|
+
|
|
26
|
+
/** 本包的 npm 名(命令/段的定义 id 用它,插件实例的 data 归属也用它)。 */
|
|
27
|
+
const PACKAGE = 'dsh-plugin-fly-review'
|
|
28
|
+
/** 收口工具名。**常驻注册**(模式关闭时也在),所以进/出姿态不改工具目录。 */
|
|
29
|
+
export const EXIT_REVIEW_MODE = 'exit_review_mode'
|
|
30
|
+
/** 会话日志里的事件名;投影键同源。 */
|
|
31
|
+
const MODE_EVENT = 'review/mode'
|
|
32
|
+
const PROJECTION_KEY = 'review'
|
|
33
|
+
/** 提示词段的段名与槽位(槽位沿用上游那一族的 policy 槽:`getSectionOrder` 是按名取值的具名槽)。 */
|
|
34
|
+
const SECTION_NAME = 'review:policy'
|
|
35
|
+
const SECTION_ORDER = 'PLAN_POLICY'
|
|
36
|
+
/** 命令名。斜杠菜单里就是这个。 */
|
|
37
|
+
const COMMAND_NAME = 'review'
|
|
38
|
+
/** 裁决卡的选项标签:**逐字相等**分派(前缀只能当手打兜底;改了文案必须同改分支)。 */
|
|
39
|
+
const ACCEPT_LABEL = '接受评审并退出'
|
|
40
|
+
const KEEP_LABEL = '继续评审'
|
|
41
|
+
const REVIEW_QUESTION_ID = 'review-mode'
|
|
42
|
+
/** 默认纪律文本(行配置 `section` 可整段覆写)。 */
|
|
43
|
+
export const DEFAULT_SECTION = [
|
|
44
|
+
'You are reviewing, not authoring: this session is in review mode.',
|
|
45
|
+
'',
|
|
46
|
+
'Read-only posture: do not edit files, do not run commands with side effects, and do not "fix it while you are there".',
|
|
47
|
+
'If something must be changed, say what and why — the change itself happens after the review.',
|
|
48
|
+
'',
|
|
49
|
+
'Order your findings:',
|
|
50
|
+
'1. Blocking problems first — what cannot ship, what is provably wrong, what is missing.',
|
|
51
|
+
'2. Then concrete additions, each one actionable.',
|
|
52
|
+
'',
|
|
53
|
+
'Every claim carries evidence: a `path:line`, a command with its output, or steps that reproduce it.',
|
|
54
|
+
'No praise. Do not restate the thing under review. If you found nothing blocking, say that plainly.',
|
|
55
|
+
'',
|
|
56
|
+
'Finish by handing the verdict to the user with `exit_review_mode`; their decision ends the posture.',
|
|
57
|
+
].join('\n')
|
|
58
|
+
|
|
59
|
+
const REVIEW_DESCRIPTION = `Use only in review mode. Present your review verdict to the user with ${EXIT_REVIEW_MODE}: blocking problems first, then additions, every claim with evidence. On acceptance the review posture ends (the verdict does not authorise any further change by itself).`
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 校验行配置。`section` 可省(用内置纪律文本 ✓);**未知键直接抛** —— 配置写错了要在
|
|
63
|
+
* 加载期就大声失败,而不是被忽略(照上游 `resolveConfig` 的口径)。
|
|
64
|
+
*/
|
|
65
|
+
export function resolveConfig(config = {}) {
|
|
66
|
+
const section = config.section ?? DEFAULT_SECTION
|
|
67
|
+
if (typeof section !== 'string' || section.trim() === '') {
|
|
68
|
+
throw new Error(`${PACKAGE}: config.section must be a non-empty string (omit it to use the built-in discipline text)`)
|
|
69
|
+
}
|
|
70
|
+
const unknown = Object.keys(config).filter(key => key !== 'section')
|
|
71
|
+
if (unknown.length > 0) throw new Error(`${PACKAGE}: unknown config key(s) ${unknown.join(', ')} — config is { section }`)
|
|
72
|
+
return { section }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 会话日志投影:折叠命令与事件,重建"现在是不是评审姿态"。 */
|
|
76
|
+
export const reviewProjectionDefinition = {
|
|
77
|
+
key: PROJECTION_KEY,
|
|
78
|
+
stateVersion: 1,
|
|
79
|
+
stateSchema: z.object({
|
|
80
|
+
active: z.boolean(),
|
|
81
|
+
wanted: z.boolean().nullable(),
|
|
82
|
+
running: z.object({ commandId: z.string(), wanted: z.boolean() }).strict().nullable(),
|
|
83
|
+
activeAtLastHeader: z.boolean().nullable(),
|
|
84
|
+
}).strict(),
|
|
85
|
+
init: () => ({ active: false, wanted: null, running: null, activeAtLastHeader: null }),
|
|
86
|
+
apply: (state, event) => {
|
|
87
|
+
if (event.type === 'command/run' && event.data.name === COMMAND_NAME) {
|
|
88
|
+
if (event.data.args === undefined) return state
|
|
89
|
+
return { ...state, running: { commandId: event.data.commandId, wanted: event.data.args.trim() !== 'off' } }
|
|
90
|
+
}
|
|
91
|
+
if (event.type === 'command/done' && event.data.commandId === state.running?.commandId) {
|
|
92
|
+
const wanted = event.data.kind === 'success' && state.running.wanted !== state.active ? state.running.wanted : null
|
|
93
|
+
return { ...state, wanted, running: null }
|
|
94
|
+
}
|
|
95
|
+
if (event.type === MODE_EVENT) return { ...state, active: event.data.active, wanted: null }
|
|
96
|
+
if (event.type === 'request/header') return { ...state, activeAtLastHeader: state.active }
|
|
97
|
+
return state
|
|
98
|
+
},
|
|
99
|
+
wire: {
|
|
100
|
+
viewSchema: z.object({ active: z.boolean(), pending: z.boolean() }),
|
|
101
|
+
view: state => ({ active: state.active, pending: state.wanted !== null }),
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 评审姿态的服务:拥有日志态、段、命令与收口工具。 */
|
|
106
|
+
export class ReviewModeController extends Service {
|
|
107
|
+
static inject = ['tools', 'systemPrompt', 'sessionProjections']
|
|
108
|
+
|
|
109
|
+
/** 校验过的纪律文本(默认内置,行配置可覆写)。 */
|
|
110
|
+
section
|
|
111
|
+
|
|
112
|
+
/** 等待"下一个被接受的 turn 边界"落地的选择(`narrate` = 是否顺带注入一句通知)。 */
|
|
113
|
+
pendingIntents = new WeakMap()
|
|
114
|
+
|
|
115
|
+
constructor(ctx, config = {}) {
|
|
116
|
+
super(ctx, 'reviewMode')
|
|
117
|
+
this.section = resolveConfig(config).section
|
|
118
|
+
let disposed = false
|
|
119
|
+
|
|
120
|
+
// ① 边界:下一个被接受的 turn 之前把待定选择写进日志(同一步里前后一致)。
|
|
121
|
+
ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
|
|
122
|
+
const decision = await next()
|
|
123
|
+
const pending = this.pendingIntents.get(agent.session)
|
|
124
|
+
if (decision.kind === 'reject' || signal.aborted || pending === undefined) return decision
|
|
125
|
+
let narration
|
|
126
|
+
try {
|
|
127
|
+
narration = this.onBoundary(agent.session)
|
|
128
|
+
} catch (error) {
|
|
129
|
+
ctx.logger.warn(`${PACKAGE}: failed to append the selected review mode at step start: %o`, error)
|
|
130
|
+
return decision
|
|
131
|
+
}
|
|
132
|
+
return !pending.narrate || narration === undefined ? decision : { ...decision, messages: [...decision.messages, narration] }
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
ctx.effect(() => () => { disposed = true }, `${PACKAGE}: close service lifetime`)
|
|
136
|
+
|
|
137
|
+
// ② 引导段:只在姿态激活时非空(进/出姿态的全部效果就是这个)。
|
|
138
|
+
ctx.systemPrompt.section({
|
|
139
|
+
name: SECTION_NAME,
|
|
140
|
+
order: ctx.systemPrompt.getSectionOrder(SECTION_ORDER),
|
|
141
|
+
text: (context) => {
|
|
142
|
+
if (context.agent === undefined) return ''
|
|
143
|
+
const active = this.pendingIntents.get(context.agent.session)?.active ?? this.loggedActive(context.agent.session)
|
|
144
|
+
return active ? this.section : ''
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// ③ 状态进会话日志 ⇒ resume/fork 自动恢复。
|
|
149
|
+
ctx.sessionProjections.register(reviewProjectionDefinition)
|
|
150
|
+
|
|
151
|
+
// ④ 命令:`/review` 开,`/review off` 关,`/review <文字>` 附带一句焦点。
|
|
152
|
+
ctx.inject(['commands'], (commandCtx) => {
|
|
153
|
+
commandCtx.commands.register({
|
|
154
|
+
definitionId: brandString(PACKAGE),
|
|
155
|
+
name: COMMAND_NAME,
|
|
156
|
+
description: 'Enter or leave review mode',
|
|
157
|
+
input: { hint: '[off|focus]', attachments: true },
|
|
158
|
+
handler: ({ agent, rawInput, attachments }) => {
|
|
159
|
+
const message = rawInput.trim()
|
|
160
|
+
if (message === 'off' && attachments.length > 0) {
|
|
161
|
+
return { kind: 'error', text: 'Attachments cannot accompany /review off.' }
|
|
162
|
+
}
|
|
163
|
+
if (message === 'off') {
|
|
164
|
+
switch (this.set(agent, false)) {
|
|
165
|
+
case 'committed': return { kind: 'success', text: 'Review mode off.' }
|
|
166
|
+
case 'queued': return { kind: 'success', text: 'Leaving review mode (applies from the next step).' }
|
|
167
|
+
case 'cancelled': return { kind: 'success', text: 'Review mode entry cancelled.' }
|
|
168
|
+
default: return this.loggedActive(agent.session)
|
|
169
|
+
? { kind: 'success', text: 'Leaving review mode (applies from the next step).' }
|
|
170
|
+
: { kind: 'success', text: 'Review mode is already inactive.' }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const outcome = this.set(agent, true)
|
|
174
|
+
if (message !== '' || attachments.length > 0) {
|
|
175
|
+
agent.steer(createUserMessage({
|
|
176
|
+
content: [...attachments, ...(message === '' ? [] : [{ type: 'text', text: message }])],
|
|
177
|
+
source: { kind: PACKAGE },
|
|
178
|
+
}))
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
kind: 'success',
|
|
182
|
+
text: outcome === 'committed'
|
|
183
|
+
? 'Review mode on. Use /review off to leave. (The posture is guidance: the sandbox and approval policy still decide what runs.)'
|
|
184
|
+
: 'Entering review mode (applies from the next step). Use /review off to leave.',
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// ⑤ 收口工具:常驻注册,把判词交用户裁决。
|
|
191
|
+
ctx.tools.register(defineTool({
|
|
192
|
+
name: EXIT_REVIEW_MODE,
|
|
193
|
+
description: REVIEW_DESCRIPTION,
|
|
194
|
+
parameters: {
|
|
195
|
+
review: { type: 'string', required: true, description: 'The complete review verdict, as markdown, starting with a # heading that names it.' },
|
|
196
|
+
},
|
|
197
|
+
output: {
|
|
198
|
+
schema: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
additionalProperties: false,
|
|
201
|
+
properties: { accepted: { type: 'boolean', const: true, required: true } },
|
|
202
|
+
},
|
|
203
|
+
// 收口旁白:这句是**给模型的指令**,所以口径必须是我们自己的 —— 接受评审**不授权**任何改动
|
|
204
|
+
//(上游 plan 模式这里的原文是 "carry out the plan starting with your next step",评审不能这么写 ✗)。
|
|
205
|
+
render: () => [{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: 'Review accepted — the review posture is over. The verdict is the user\'s now; acceptance by itself authorises no further change.',
|
|
208
|
+
}],
|
|
209
|
+
},
|
|
210
|
+
// **箭头函数**:`defineTool` 调 execute 时不绑 this(上游 plan-mode 同样用箭头 ✓)。
|
|
211
|
+
// 写成方法简写 ⇒ 运行期 `this` 是 undefined ⇒ 收口工具一调用就炸 ✗(本轮单测抓到的真 bug)。
|
|
212
|
+
execute: async (args, exec) => {
|
|
213
|
+
const agent = exec.agent
|
|
214
|
+
if (agent === undefined) throw new Error(`${EXIT_REVIEW_MODE} requires a calling agent (no session to switch)`)
|
|
215
|
+
if (!this.loggedActive(agent.session)) throw new Error(`${EXIT_REVIEW_MODE} is only available in review mode`)
|
|
216
|
+
if (!/^#\s+\S/.test(args.review.trim())) throw new Error(`${EXIT_REVIEW_MODE} requires a non-empty markdown review starting with a # heading`)
|
|
217
|
+
const interaction = ctx.get('userQuestions')
|
|
218
|
+
if (interaction === undefined) throw new Error('no user-questions channel is available to decide the review; ask the user to switch the session mode instead')
|
|
219
|
+
const answer = await interaction.ask({
|
|
220
|
+
questions: [{
|
|
221
|
+
id: REVIEW_QUESTION_ID,
|
|
222
|
+
header: '评审裁决',
|
|
223
|
+
question: '接受这份评审并退出评审姿态?',
|
|
224
|
+
detail: args.review,
|
|
225
|
+
options: [
|
|
226
|
+
{ label: ACCEPT_LABEL, description: '退出评审姿态。判词归你;这本身不授权任何后续改动。' },
|
|
227
|
+
{ label: KEEP_LABEL, description: '留在评审姿态,你的反馈回到模型,继续看。' },
|
|
228
|
+
],
|
|
229
|
+
intent: { kind: 'review-mode', accept: ACCEPT_LABEL, callId: exec.callId },
|
|
230
|
+
}],
|
|
231
|
+
agent,
|
|
232
|
+
signal: exec.signal,
|
|
233
|
+
}).catch((cause) => {
|
|
234
|
+
if (cause instanceof UserQuestionError && cause.code === 'ASK_CANCELLED') throw new Error('The user dismissed the review card to speak instead; stay in review mode and keep going.')
|
|
235
|
+
throw cause
|
|
236
|
+
})
|
|
237
|
+
if (disposed) throw new Error('the review-mode service was reloaded while the review was under decision; present the review again')
|
|
238
|
+
const items = answer.answers.filter(entry => entry.id === REVIEW_QUESTION_ID)
|
|
239
|
+
const item = items.length === 1 ? items[0] : undefined
|
|
240
|
+
if (item?.selected.length !== 1 || item.selected[0] !== ACCEPT_LABEL || item.custom !== undefined) {
|
|
241
|
+
const feedback = item?.custom ?? ''
|
|
242
|
+
throw new Error(feedback === ''
|
|
243
|
+
? 'The user chose to keep reviewing; revise the review and present it again.'
|
|
244
|
+
: `The user chose to keep reviewing; their feedback: ${feedback}`)
|
|
245
|
+
}
|
|
246
|
+
this.pendingIntents.set(agent.session, { active: false, narrate: false })
|
|
247
|
+
return { accepted: true }
|
|
248
|
+
},
|
|
249
|
+
}))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 日志态 + 待定选择。 */
|
|
253
|
+
get(agent) {
|
|
254
|
+
const active = this.loggedActive(agent.session)
|
|
255
|
+
const pending = this.pendingIntents.get(agent.session)
|
|
256
|
+
return pending === undefined ? { active } : { active, pending: pending.active }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 选择评审姿态是否激活。
|
|
261
|
+
* 回合之间直接落日志(不会再有 in-turn pre-step);回合开着就排队等下一个被接受的 pre-step。
|
|
262
|
+
* 重复选择当前/已待定的状态是 no-op。
|
|
263
|
+
*/
|
|
264
|
+
set(agent, active) {
|
|
265
|
+
const session = agent.session
|
|
266
|
+
if (active === (this.pendingIntents.get(session)?.active ?? this.loggedActive(session))) return 'noop'
|
|
267
|
+
if (this.hasOpenTurn(session)) {
|
|
268
|
+
this.pendingIntents.set(session, { active, narrate: true })
|
|
269
|
+
return this.loggedActive(session) === active ? 'cancelled' : 'queued'
|
|
270
|
+
}
|
|
271
|
+
if (active === this.loggedActive(session)) {
|
|
272
|
+
this.pendingIntents.delete(session)
|
|
273
|
+
return 'cancelled'
|
|
274
|
+
}
|
|
275
|
+
session.append(MODE_EVENT, { active })
|
|
276
|
+
this.pendingIntents.delete(session)
|
|
277
|
+
const narration = this.narration(session, active)
|
|
278
|
+
if (narration !== undefined) agent.inject(narration)
|
|
279
|
+
return 'committed'
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 把待定选择写进日志(下一个请求组装之前)。返回要追加的通知消息(若有)。 */
|
|
283
|
+
onBoundary(session) {
|
|
284
|
+
const pending = this.pendingIntents.get(session)
|
|
285
|
+
if (pending === undefined) return undefined
|
|
286
|
+
const target = pending.active
|
|
287
|
+
this.pendingIntents.delete(session)
|
|
288
|
+
if (target === this.loggedActive(session)) return undefined
|
|
289
|
+
session.append(MODE_EVENT, { active: target })
|
|
290
|
+
return this.narration(session, target)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** 用户切远近态时的通知(只在"上一次请求头描述的是另一种姿态"时才说)。 */
|
|
294
|
+
narration(session, target) {
|
|
295
|
+
const told = this.loggedActiveAtLastHeader(session)
|
|
296
|
+
if (told === undefined || told === target) return undefined
|
|
297
|
+
const text = target
|
|
298
|
+
? 'The user switched this session into review mode: read-only posture, blocking problems first, evidence for every claim.'
|
|
299
|
+
: 'The user switched this session back out of review mode.'
|
|
300
|
+
return createUserMessage({
|
|
301
|
+
content: [{ type: 'text', text }],
|
|
302
|
+
source: { kind: PACKAGE, form: 'notice', summary: text },
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
loggedActive(session) {
|
|
307
|
+
return this.reviewState(session).active
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
loggedActiveAtLastHeader(session) {
|
|
311
|
+
return this.reviewState(session).activeAtLastHeader ?? undefined
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
hasOpenTurn(session) {
|
|
315
|
+
const state = this.ctx.sessionProjections.stateOf(session, 'turnBoundary')
|
|
316
|
+
if (state === undefined) throw new Error(`${PACKAGE} requires the turnBoundary session projection`)
|
|
317
|
+
return state.openTurnStartSeq !== null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 读本包的投影态;读不到就**大声抛**(静默返回 false 会让姿态看起来"没生效"却查不出原因)。 */
|
|
321
|
+
reviewState(session) {
|
|
322
|
+
const state = this.ctx.sessionProjections.stateOf(session, PROJECTION_KEY)
|
|
323
|
+
if (state === undefined) throw new Error(`${PACKAGE} requires the ${PROJECTION_KEY} session projection`)
|
|
324
|
+
return state
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export default ReviewModeController
|