@lynn123411/dsh-ask-user-grilling 0.1.1

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 (3) hide show
  1. package/README.md +24 -0
  2. package/lib/index.js +247 -0
  3. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @lynn123411/dsh-ask-user-grilling
2
+
3
+ DSH 侧的 grilling 适配层(输送机制):把 Matt Pocock 的 grilling 流程在 DSH 里的提问环节做成工具级硬约束。**不修改、不包装这组技能的任何文件**——技能按原设计运行,本插件只负责「在 DSH 里怎么问」。
4
+
5
+ ## 特性
6
+
7
+ - **ask_user_grilling**:grilling 轮次专用提问工具。
8
+ - **子代理闸门**:后台有子代理运行时会拒绝提问,返回「阻塞 + 运行中名单」——agent 应**结束当前回合**,子代理结算通知自动唤醒后再调用,不在回合内反复重试;
9
+ - **强制多选**:所有问题一律多选(schema 不提供关闭开关);
10
+ - **补充机制**:自动为每个问题追加「补充」选项,并自动在每轮末尾追加「本轮还有什么要补充或调整的吗?」;
11
+ - **题干引导**:要求题干只含问题本身、不重复选项文本(仅模型侧引导,不做硬校验——避免误伤自然提及选项名称的题干)。
12
+ - **enter_plan_mode**:为当前 agent 激活 DSH 计划模式。grilling 最后一轮答完、用户确认共识后调用,让 agent 写方案供审阅而不是直接开始执行;可通过 `exit_plan_mode` 或 `/plan off` 退出。
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ dsh plugin --profile web add @lynn123411/dsh-ask-user-grilling
18
+ ```
19
+
20
+ 已发布至 npm,配合 `matt-standard` 预设使用(preset 的工具行直接消费本包)。
21
+
22
+ **本地开发**:将本目录复制到 `~/.dsh/profiles/web/node_modules/@lynn123411/dsh-ask-user-grilling/` 即可,preset 的工具行会直接从 node_modules 解析。注意:**不要**把它加进 profile `package.json` 的 `dsh.profile.bundles`(bundle 层必须在包内声明 `dsh.bundle`,否则启动报错)。
23
+
24
+ > 注意:profile 是 pnpm hoisted 布局:`pnpm install` / `dsh plugin add|remove` 重装后,手工同步的本地副本会被清掉,需重新同步。
package/lib/index.js ADDED
@@ -0,0 +1,247 @@
1
+ import { defineTool } from "@deepseek-ai/dsh-tools";
2
+ import "@deepseek-ai/dsh-user-questions";
3
+
4
+ /**
5
+ * @lynn123411/dsh-ask-user-grilling — DSH delivery adaptations for grilling
6
+ * rounds (mattpocock/skills). No skill file is modified; this package only
7
+ * hardens how rounds are asked inside DSH.
8
+ *
9
+ * ask_user_grilling:
10
+ * - gate: refuses while background subagents are running (R1)
11
+ * - forces multi-select on every question (R2)
12
+ * - appends a per-question "Supplement" option and a round-end question (R3)
13
+ * - stem/option separation is guidance only — never rejects stems (R4
14
+ * relaxed: substring checks false-positive on legitimate stems)
15
+ *
16
+ * enter_plan_mode:
17
+ * - activates DSH plan mode for the current agent (planMode.set)
18
+ */
19
+ const name = "tool-ask-user-grilling";
20
+ const inject = ["tools", "userQuestions"];
21
+
22
+ const SUPPLEMENT_OPTION = {
23
+ label: "Supplement",
24
+ description: "Select this and write your supplement for this question in your reply.",
25
+ };
26
+
27
+ const ROUND_END_QUESTION = {
28
+ id: "__grill_round_supplement__",
29
+ question: "Anything else to add or adjust this round?",
30
+ header: "Round supplement",
31
+ options: [
32
+ { label: "Nothing to add" },
33
+ { label: "I have something to add (write it in my reply)", description: "Select this and write any additions or adjustments for this round in your reply." },
34
+ ],
35
+ multiSelect: true,
36
+ };
37
+
38
+ function displayName(entry) {
39
+ return entry.label ?? entry.id;
40
+ }
41
+
42
+ function apply(ctx) {
43
+ ctx.tools.register(defineTool({
44
+ name: "ask_user_grilling",
45
+ description: "Ask the user a ROUND of grilling questions (decision-tree interview). Use for grilling sessions (grill-me / grill-with-docs / triage / wayfinder / architecture grilling): send ALL frontier questions of the current round in ONE call. The tool forces multi-select, appends a per-question 'Supplement' option, and appends a round-end supplement question automatically. If background subagents are running, this tool returns blocked — do NOT retry within the same turn: end your turn and wait; the settlement notice will wake you automatically, then call again. Keep the question stem to the question itself, without repeating option labels (style guidance only — stems are never rejected). Example of one round with two questions: questions: [{ id: 'q1', question: 'Which issue tracker?', options: [{ label: 'GitHub' }, { label: 'Local markdown' }] }, { id: 'q2', question: 'Any deadline?', options: [{ label: 'This week' }, { label: 'Next month' }] }].",
46
+ parameters: {
47
+ questions: {
48
+ type: "array",
49
+ required: true,
50
+ description: "All frontier questions of the current round, each with a stable id, the question text (stem) and options.",
51
+ items: {
52
+ type: "object",
53
+ additionalProperties: true,
54
+ properties: {
55
+ id: {
56
+ type: "string",
57
+ required: true,
58
+ description: "Stable id for this question; echoed in the answer. Must NOT start with __grill_ (reserved prefix).",
59
+ },
60
+ question: {
61
+ type: "string",
62
+ required: true,
63
+ description: "The question stem only — never include option text here.",
64
+ },
65
+ header: {
66
+ type: "string",
67
+ description: "Optional short heading for the question, such as \"Round 3\".",
68
+ },
69
+ options: {
70
+ type: "array",
71
+ description: "Choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.",
72
+ items: {
73
+ type: "object",
74
+ additionalProperties: true,
75
+ properties: {
76
+ label: {
77
+ type: "string",
78
+ required: true,
79
+ description: "Short user-facing option label.",
80
+ },
81
+ description: {
82
+ type: "string",
83
+ description: "One sentence explaining the tradeoff or impact.",
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ output: {
93
+ schema: {
94
+ type: "object",
95
+ additionalProperties: false,
96
+ properties: {
97
+ blocked: {
98
+ type: "boolean",
99
+ description: "True when background subagents are running and the round was not asked.",
100
+ },
101
+ waiting: {
102
+ type: "array",
103
+ items: { type: "string" },
104
+ description: "Running subagent display names when blocked.",
105
+ },
106
+ rejected: {
107
+ type: "boolean",
108
+ description: "True when input validation failed and the round was not asked.",
109
+ },
110
+ violations: {
111
+ type: "array",
112
+ items: { type: "string" },
113
+ description: "Validation violations when rejected (reserved id prefix only).",
114
+ },
115
+ error: { type: "string" },
116
+ answers: {
117
+ type: "array",
118
+ items: {
119
+ type: "object",
120
+ additionalProperties: false,
121
+ properties: {
122
+ id: { type: "string", required: true },
123
+ selected: { type: "array", required: true, items: { type: "string" } },
124
+ custom: { type: "string" },
125
+ },
126
+ },
127
+ },
128
+ },
129
+ },
130
+ render: (_args, value) => [{ type: "text", text: JSON.stringify(value) }],
131
+ },
132
+ async execute(args, exec) {
133
+ // 1. gate: refuse while background subagents are running (R1)
134
+ const subagents = ctx.get("subagents");
135
+ const agent = exec.agent;
136
+ const waiting = [];
137
+ if (subagents !== undefined && agent !== undefined) {
138
+ try {
139
+ const entries = await subagents.listDescendants(agent.id, exec.signal);
140
+ for (const entry of entries) {
141
+ if (entry.kind === "child" && entry.activity === "running") waiting.push(displayName(entry));
142
+ }
143
+ } catch (error) {
144
+ // Fail-closed: R1 says never enter the decision tree unless we know
145
+ // no subagents are running. Only an aborted call (user cancelled)
146
+ // propagates; any other listing failure blocks the round.
147
+ if (exec.signal.aborted) throw error;
148
+ console.error("ask_user_grilling: subagent listing failed: %o", error);
149
+ return {
150
+ blocked: true,
151
+ waiting: [],
152
+ error: "Cannot confirm background subagent status (subagents query failed). Please call this tool again later.",
153
+ };
154
+ }
155
+ }
156
+ if (waiting.length > 0) {
157
+ return {
158
+ blocked: true,
159
+ waiting,
160
+ error: `Still ${waiting.length} subagent(s) running (${waiting.join(", ")}). End your turn and wait — the settlement notice will wake you automatically, then call again.`,
161
+ };
162
+ }
163
+
164
+ // 2. input validation: reserved id prefix guard only (the round-end
165
+ // question owns __grill_). Stem/option separation (R4) is guidance,
166
+ // NOT enforced: substring matching rejected legitimate stems (e.g. a
167
+ // stem that naturally mentions an option name), so no stem check may
168
+ // refuse a round — a bad stem is preferable to a false rejection.
169
+ const violations = [];
170
+ for (const question of args.questions) {
171
+ if (typeof question.id === "string" && question.id.startsWith("__grill_")) {
172
+ violations.push(`Question id "${question.id}" uses the reserved prefix __grill_`);
173
+ }
174
+ }
175
+ if (violations.length > 0) {
176
+ return {
177
+ rejected: true,
178
+ violations,
179
+ error: "Question ids must not use the reserved prefix __grill_ (reserved for the round-end supplement question). Fix the ids and call this tool again.",
180
+ };
181
+ }
182
+
183
+ // 3. transform: force multi-select (R2), append supplement option (R3)
184
+ const questions = args.questions.map((question) => ({
185
+ id: question.id,
186
+ question: question.question,
187
+ ...(question.header !== undefined ? { header: question.header } : {}),
188
+ options: [
189
+ ...(question.options ?? []).map((option) => ({
190
+ label: option.label,
191
+ ...(option.description !== undefined ? { description: option.description } : {}),
192
+ })),
193
+ SUPPLEMENT_OPTION,
194
+ ],
195
+ multiSelect: true,
196
+ }));
197
+
198
+ // 4. round-end supplement question (R3)
199
+ questions.push(ROUND_END_QUESTION);
200
+
201
+ // 5. ask through the userQuestions seam (UI renders from the service)
202
+ const answer = await ctx.userQuestions.ask({
203
+ questions,
204
+ ...(exec.agent !== undefined ? { agent: exec.agent } : {}),
205
+ signal: exec.signal,
206
+ });
207
+ return {
208
+ answers: answer.answers.map((entry) => ({
209
+ id: entry.id,
210
+ selected: [...entry.selected],
211
+ ...(entry.custom !== undefined ? { custom: entry.custom } : {}),
212
+ })),
213
+ };
214
+ },
215
+ }));
216
+
217
+ ctx.tools.register(defineTool({
218
+ name: "enter_plan_mode",
219
+ description: "Enter plan mode for the current agent. Call it after the final round of a grilling session once the user has confirmed shared understanding, so a plan is written for review instead of executing directly. Plan mode ends via exit_plan_mode or the /plan off command.",
220
+ parameters: {},
221
+ output: {
222
+ schema: {
223
+ type: "object",
224
+ additionalProperties: false,
225
+ properties: {
226
+ ok: { type: "boolean" },
227
+ result: { type: "string" },
228
+ error: { type: "string" },
229
+ },
230
+ },
231
+ render: (_args, value) => [{ type: "text", text: JSON.stringify(value) }],
232
+ },
233
+ async execute(_args, exec) {
234
+ const planMode = ctx.get("planMode");
235
+ if (planMode === undefined) return { ok: false, error: "planMode service unavailable" };
236
+ const agent = exec.agent;
237
+ if (agent === undefined) return { ok: false, error: "current agent unavailable" };
238
+ const outcome = planMode.set(agent, true);
239
+ // cancelled = an opposite pending selection was cleared and the logged
240
+ // state already matches the requested active state — plan mode IS active.
241
+ const ok = outcome === "committed" || outcome === "queued" || outcome === "cancelled" || outcome === "noop";
242
+ return { ok, result: `plan mode ${outcome}` };
243
+ },
244
+ }));
245
+ }
246
+
247
+ export { apply, inject, name };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@lynn123411/dsh-ask-user-grilling",
3
+ "version": "0.1.1",
4
+ "description": "DSH delivery adaptations for grilling rounds: ask_user_grilling (subagent gate / forced multi-select / supplement option / stem guidance) and enter_plan_mode. Does not modify any skill file.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "files": [
12
+ "lib",
13
+ "README.md"
14
+ ],
15
+ "license": "MIT",
16
+ "peerDependencies": {
17
+ "@deepseek-ai/cordis": "^4.0.1",
18
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.1",
19
+ "@deepseek-ai/dsh-user-questions": "^0.1.2-alpha.1"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ }
24
+ }