@widnyana/design-thinking 0.0.6 → 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.
- package/GUIDE.md +233 -0
- package/README.md +46 -9
- package/extensions/design-thinking.ts +491 -76
- package/package.json +3 -2
- package/prompts/cg-map.md +8 -8
- package/prompts/cg-plan.md +52 -32
- package/prompts/cg-review.md +13 -8
- package/prompts/cg.md +50 -13
- package/references/design-graph.md +199 -77
- package/references/{method.md → design-thinking.md} +32 -28
- package/references/effect-ts.md +2 -2
- package/references/graph-protocol.md +302 -0
- package/references/meta-research.md +692 -0
- package/references/{protocol.md → output-format.md} +92 -10
- package/skills/design-graph/SKILL.md +204 -82
- package/skills/{design-method → design-thinking}/SKILL.md +41 -36
- package/skills/graph-protocol/SKILL.md +245 -170
|
@@ -1,28 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Design Thinking — pi extension
|
|
3
3
|
*
|
|
4
|
-
* Adopts
|
|
5
|
-
* - Design Thinking :
|
|
6
|
-
* - Design Graph :
|
|
7
|
-
* - Graph Protocol :
|
|
4
|
+
* Adopts r17x's three methods (one shape, three materials):
|
|
5
|
+
* - Design Thinking : code, X → Graph → Effect<A, E, R> (§1-§10 pipeline)
|
|
6
|
+
* - Design Graph : interface, Job → Flow → Surface<C, V, N>
|
|
7
|
+
* - Graph Protocol : delegation, AGENT SUBGRAPH in, IMPLEMENTED GRAPH out
|
|
8
8
|
*
|
|
9
9
|
* When /dt mode is active, every turn appends a compact distilled block to the
|
|
10
|
-
* system prompt so design/plan/review answers render as
|
|
11
|
-
*
|
|
10
|
+
* system prompt so design/plan/review answers render as an annotated call
|
|
11
|
+
* graph in the Design Thinking output format. Full specs live in the package's references/ directory,
|
|
12
12
|
* loaded on demand — not inlined here, to keep the injected prompt small.
|
|
13
|
+
* Separately, an ALWAYS-ON LSP reminder composes into every run's system
|
|
14
|
+
* prompt while the harness exposes an `lsp` tool (mode-independent); without
|
|
15
|
+
* one, the user is told once per session how to enable it.
|
|
13
16
|
*
|
|
14
17
|
* Commands:
|
|
18
|
+
* /research-prompt <msg> Meta Research Prompt Generator (references/meta-research.md)
|
|
15
19
|
* /dt toggle Design Thinking mode
|
|
16
20
|
* /dt on|off set explicitly
|
|
17
|
-
* /dt
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* /dt approve|deny manual arm/revoke fallback (headless / no-UI sessions)
|
|
22
|
+
* After a gated run presents a design graph, a review dialog opens
|
|
23
|
+
* automatically: Approve (arm + implement), Refine (send feedback), or
|
|
24
|
+
* Deny (keep file edits blocked). /dt <prompt> turns the mode ON (never
|
|
25
|
+
* off) and runs the prompt — graph + clarifying questions before code.
|
|
26
|
+
*
|
|
27
|
+
* Approval gate design (one state machine, one owner):
|
|
28
|
+
* - The gate is `{ mode: "off" | "on", approval: "none" | "armed" }`. Every
|
|
29
|
+
* change goes through a transition function; handlers never touch the
|
|
30
|
+
* fields directly.
|
|
31
|
+
* - Approval is scoped to ONE human turn. It survives the many runs of
|
|
32
|
+
* implementing an approved design, and is re-locked (relock()) when a new
|
|
33
|
+
* user instruction arrives — a genuine `input` event, not the extension's
|
|
34
|
+
* own go-ahead / refine messages. The next instruction is a refinement:
|
|
35
|
+
* the agent updates and re-presents the graph, then the dialog re-opens.
|
|
36
|
+
* - At most one review dialog exists. Each offer carries an epoch; a dialog
|
|
37
|
+
* decision is applied only if its epoch is still current AND the gate is
|
|
38
|
+
* still on AND still unapproved. Stale decisions are discarded, not
|
|
39
|
+
* applied.
|
|
40
|
+
* - The dialog opens at run end when the agent presented a design graph
|
|
41
|
+
* (VERDICT + section headers) OR ended the turn asking for approval in
|
|
42
|
+
* prose — the human always gets a button, never a prose-only ask.
|
|
43
|
+
* - Dialogs are opened with an extension-owned AbortSignal: omp binds
|
|
44
|
+
* handler-opened dialogs to the handler's 30s abort scope, and a floating
|
|
45
|
+
* promise does not escape that binding. If the dialog is still dismissed
|
|
46
|
+
* (runtime kill, or the agent's own dialog displacing it), the offer is
|
|
47
|
+
* retracted and a fresh one opens at the next run end. No reopen loops,
|
|
48
|
+
* no timing heuristics.
|
|
49
|
+
* - An explicit Deny latches until a NEW graph is presented: deny means
|
|
50
|
+
* stop asking.
|
|
21
51
|
*
|
|
22
52
|
* Stack-agnostic: no language detection, no auto-enable. Adapt the vocabulary
|
|
23
53
|
* to whatever stack the project uses; never the discipline.
|
|
24
54
|
*/
|
|
25
55
|
|
|
56
|
+
import fs from "node:fs";
|
|
26
57
|
import path from "node:path";
|
|
27
58
|
import { fileURLToPath } from "node:url";
|
|
28
59
|
|
|
@@ -48,7 +79,7 @@ const DISTILLED = `
|
|
|
48
79
|
DESIGN THINKING MODE — active.
|
|
49
80
|
Read the problem. Draw the data flow as a call graph. Write code that IS the graph.
|
|
50
81
|
|
|
51
|
-
X →
|
|
82
|
+
X → Graph → Effect<A, E, R>
|
|
52
83
|
│ │ │ │ │
|
|
53
84
|
│ │ │ │ └─ R: what each node needs (§5)
|
|
54
85
|
│ │ │ └──── E: where the graph breaks (§4)
|
|
@@ -62,65 +93,287 @@ Pipeline: shapes → graph(A) → cardinality → E(⟳retry ↯escape ☠die)
|
|
|
62
93
|
🔒 parse at boundaries → ⛈ orthogonal behavior → scope → test-R → verdict.
|
|
63
94
|
|
|
64
95
|
Rules:
|
|
65
|
-
- ANY turn that will end in code or file
|
|
96
|
+
- ANY turn that will end in code or file changes (feature, change, fix,
|
|
66
97
|
refactor — the model does NOT get to reclassify the request): render the
|
|
67
|
-
|
|
68
|
-
ZERO
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
graph in the Design Thinking output format and any clarifying questions FIRST;
|
|
99
|
+
ZERO mutations until the user approves the design — no file edits, no
|
|
100
|
+
shell commands, no code execution. Reading/exploring code first is fine,
|
|
101
|
+
via read-only tools only (read/grep/glob; read-only MCP devices such as
|
|
102
|
+
qmd and codegraph exploration also pass). If any tool call is blocked by
|
|
103
|
+
the gate, STOP: do NOT route the change around it via bash, eval, scripts,
|
|
104
|
+
or subagents — present or update the graph and end the turn. End the turn
|
|
105
|
+
after the graph with NO prose approval request — no "approve and I'll…", no
|
|
106
|
+
"let me know", no go-ahead questions. A review dialog opens automatically
|
|
107
|
+
when the run ends — never mid-run: approve (implement the presented design
|
|
108
|
+
without re-presenting the graph), refine (present an updated graph), or
|
|
109
|
+
deny (stay blocked).
|
|
110
|
+
- Design, plan, and review answers must render the graph, fixed
|
|
73
111
|
sections: PROBLEM, SHAPES, GRAPH, CARDINALITY, BOUNDARIES, BEHAVIOR, SCOPE,
|
|
74
112
|
TEST LAYERS, VERDICT.
|
|
75
113
|
- The happy path (A) stays readable; failure handling lives at defined join points.
|
|
76
114
|
- Each layer scopes its own E; inner errors never leak through.
|
|
77
115
|
- If the code doesn't match the call graph, the implementation is wrong.
|
|
78
116
|
- Adapt vocabulary to the current language/stack; never the discipline.
|
|
117
|
+
- Interface work uses the Design Graph (Surface<C, V, N>) skeleton.
|
|
118
|
+
- Delegating to subagents or workers follows Graph Protocol: AGENT SUBGRAPH
|
|
119
|
+
in, IMPLEMENTED GRAPH out, compared before the work counts as done.
|
|
79
120
|
|
|
80
121
|
Full specs — read these when a section needs detail:
|
|
81
|
-
- ${REF_DIR}/
|
|
82
|
-
- ${REF_DIR}/
|
|
83
|
-
- ${REF_DIR}/design-graph.md
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
- ${REF_DIR}/design-thinking.md (the code method, §1-§10)
|
|
123
|
+
- ${REF_DIR}/output-format.md (sections, markers, call graph block, checklist)
|
|
124
|
+
- ${REF_DIR}/design-graph.md (the interface method + skeleton)
|
|
125
|
+
- ${REF_DIR}/graph-protocol.md (delegation: execution graph, AGENT SUBGRAPH, RETURN)
|
|
126
|
+
The extension blocks file edits, shells (bash/powershell), and code
|
|
127
|
+
execution (eval) mechanically until the user approves — a review dialog
|
|
128
|
+
opens when a graph-presenting run ends. Approval covers
|
|
129
|
+
ONE human turn: it stays armed across the many runs it takes to implement
|
|
130
|
+
the approved design, and drops the moment the user sends a new instruction.
|
|
131
|
+
Treat that next instruction as a refinement of the current design: UPDATE
|
|
132
|
+
the existing design graph to fold it in and re-present it (do not start a
|
|
133
|
+
blank graph unless the goal itself changed), then end the turn and wait for
|
|
134
|
+
the dialog. /dt deny or mode off also re-lock.
|
|
135
|
+
NEVER ask the user to run /dt approve, /dt deny, or any approval command:
|
|
136
|
+
the dialog is automatic, and mentioning commands just confuses the flow.
|
|
91
137
|
NEVER read references/<skill-name>.md — no such files exist.
|
|
92
138
|
`.trim();
|
|
93
139
|
|
|
94
|
-
// pi
|
|
95
|
-
//
|
|
96
|
-
//
|
|
140
|
+
// pi + omp mutation tools — the mechanical gate's scope. While unapproved,
|
|
141
|
+
// these are blocked outright: file edits (write, edit), shells (bash, and
|
|
142
|
+
// powershell on Windows), and omp's code-execution kernel (eval). Names a
|
|
143
|
+
// harness lacks never fire — a no-op there (powershell on unix, eval on pi).
|
|
144
|
+
// Bash is NOT pattern-gated: a deny-list of shell mutators is porous by
|
|
145
|
+
// construction (redirects, package managers, interpreters — `go mod init` is
|
|
146
|
+
// the proof), and the design turn should execute nothing anyway. Read-only
|
|
147
|
+
// exploration keeps its dedicated tools (read/grep/find/ls/glob); read-only
|
|
148
|
+
// MCP devices routed through write on omp pass too (see READONLY_MCP_PATH).
|
|
97
149
|
const MUTATING_TOOLS: Record<string, true> = {
|
|
98
150
|
write: true,
|
|
99
151
|
edit: true,
|
|
152
|
+
bash: true,
|
|
153
|
+
powershell: true,
|
|
154
|
+
eval: true,
|
|
155
|
+
};
|
|
156
|
+
// omp executes MCP tools through the `write` tool as `xd://mcp__<tool>`
|
|
157
|
+
// device paths; the blanket write block would eat them. Pure knowledge
|
|
158
|
+
// lookups are carved out in the tool_call handler: qmd (retrieve/search)
|
|
159
|
+
// and codegraph's read-only actions. Everything else stays blocked — file
|
|
160
|
+
// writes and execution devices (ast_edit, lsp, debug, browser,
|
|
161
|
+
// chrome_devtools, codegraph init/index/sync/unlock); unknown codegraph
|
|
162
|
+
// actions and unparsable device args default to blocked.
|
|
163
|
+
const READONLY_MCP_PATH =
|
|
164
|
+
/^xd:\/\/mcp__(?:[\w-]+__)?(?:qmd_(?:get|multi_get|query|status|search)|codegraph_explore)$/;
|
|
165
|
+
|
|
166
|
+
const READONLY_CODEGRAPH_ACTIONS: Record<string, true> = {
|
|
167
|
+
explore: true,
|
|
168
|
+
node: true,
|
|
169
|
+
search: true,
|
|
170
|
+
files: true,
|
|
171
|
+
status: true,
|
|
172
|
+
callers: true,
|
|
173
|
+
callees: true,
|
|
174
|
+
impact: true,
|
|
175
|
+
affected: true,
|
|
176
|
+
help: true,
|
|
100
177
|
};
|
|
101
178
|
|
|
102
179
|
interface DtState {
|
|
103
180
|
enabled: boolean;
|
|
104
181
|
}
|
|
105
182
|
|
|
183
|
+
|
|
184
|
+
|
|
106
185
|
const GATE_REASON =
|
|
107
|
-
"Design Thinking mode is ON:
|
|
186
|
+
"Design Thinking mode is ON: file edits, shell commands, and code execution are blocked until the user approves the presented design. Do NOT attempt this change through any other channel — no bash or shell commands, no eval/scripts, no subagent delegation. Explore with read-only tools (read/grep/glob), present (or update and re-present) the design graph, then END YOUR TURN with NO prose approval request — an approval dialog opens for the user automatically when the run ends. NEVER ask the user to run /dt approve, /dt deny, or any command; NEVER re-attempt the blocked action this turn.";
|
|
187
|
+
|
|
188
|
+
const REVIEW_OPTIONS = [
|
|
189
|
+
"Approve — implement now",
|
|
190
|
+
"Refine — ask the agent for changes",
|
|
191
|
+
"Deny — keep file edits blocked",
|
|
192
|
+
] as const;
|
|
193
|
+
|
|
194
|
+
const GO_AHEAD_MESSAGE =
|
|
195
|
+
"Approved — implement the presented design now. (Design Thinking gate is armed; go straight to implementation, no graph re-presentation.)";
|
|
196
|
+
|
|
197
|
+
/** Always-on LSP discipline block — mode-INDEPENDENT: it composes into every
|
|
198
|
+
* run's system prompt while a language server exists, whether or not /dt is
|
|
199
|
+
* on. */
|
|
200
|
+
const LSP_REMINDER = `
|
|
201
|
+
LSP FIRST — a language server is available in this session.
|
|
202
|
+
- definition, type_definition, implementation, references, hover, symbols,
|
|
203
|
+
rename, rename_file, code_actions, diagnostics: use the \`lsp\` tool.
|
|
204
|
+
- NEVER hand-grep for definitions/references or hand-edit cross-file renames
|
|
205
|
+
when \`lsp\` answers the same question — it follows shadowing and re-exports
|
|
206
|
+
that text search misses.
|
|
207
|
+
- Imports and quick-fixes: list \`code_actions\`, apply one.
|
|
208
|
+
- If \`lsp\` reports no configured servers for this project, tell the user ONCE
|
|
209
|
+
this session how to enable one (project lsp.json or server install), then
|
|
210
|
+
continue without repeating it.
|
|
211
|
+
`.trim();
|
|
212
|
+
|
|
213
|
+
const LSP_SUGGESTION_OMP =
|
|
214
|
+
"LSP Reminder: no `lsp` tool in this session — LSP is disabled. Enable it in settings, or add a server for this project (lsp.json; auto-detect needs a root marker plus the server binary on PATH). Until then, code intelligence falls back to text search.";
|
|
215
|
+
|
|
216
|
+
const LSP_SUGGESTION_PI =
|
|
217
|
+
"LSP Reminder: pi has no LSP integration — code intelligence falls back to text search. omp provides a built-in `lsp` tool.";
|
|
218
|
+
|
|
219
|
+
// Output-format section headers (code and interface skeletons). A real graph renders the fixed set;
|
|
220
|
+
// VERDICT is mandatory. Requiring VERDICT + ≥3 others avoids tripping on a bare
|
|
221
|
+
// mention of the word "verdict" in prose, and still fires on a graph rendered
|
|
222
|
+
// without that exact literal elsewhere.
|
|
223
|
+
const SECTION_HEADER =
|
|
224
|
+
/^[ \t]*#{0,4}[ \t>*-]*(PROBLEM|SHAPES|GRAPH|CARDINALITY|BOUNDARIES|BEHAVIOR|SCOPE|TEST LAYERS|JOB|SURFACES|FLOW|VOID|NEEDS|BOUNDARY|ATTENTION|PROOF|VERDICT)\b/gm;
|
|
225
|
+
|
|
226
|
+
/** The agent is asking the human for a go-ahead in prose — narrow on purpose so
|
|
227
|
+
* an ordinary closing sentence does not trip it. */
|
|
228
|
+
const APPROVAL_ASK =
|
|
229
|
+
/\b(approve|go[- ]?ahead|await(?:ing)? (?:your )?(?:approval|go)|shall i (?:implement|proceed|write|build|start)|should i (?:implement|proceed|write|build|start)|want me to (?:implement|write|build|start|proceed)|ready to implement)\b/i;
|
|
230
|
+
|
|
231
|
+
/** Flatten an AgentMessage's content to plain text (string or content parts). */
|
|
232
|
+
function messageText(content: unknown): string {
|
|
233
|
+
if (typeof content === "string") return content;
|
|
234
|
+
if (!Array.isArray(content)) return "";
|
|
235
|
+
return content
|
|
236
|
+
.map((part) =>
|
|
237
|
+
part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string"
|
|
238
|
+
? (part as { text: string }).text
|
|
239
|
+
: "",
|
|
240
|
+
)
|
|
241
|
+
.join("\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Does an assistant message render a design graph (code or interface skeleton)? */
|
|
245
|
+
function presentsGraph(message: { role: string; content?: unknown }): boolean {
|
|
246
|
+
if (message.role !== "assistant") return false;
|
|
247
|
+
const seen = new Set<string>();
|
|
248
|
+
for (const m of messageText(message.content).matchAll(SECTION_HEADER)) seen.add(m[1]);
|
|
249
|
+
return seen.has("VERDICT") && seen.size >= 4;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Did the agent end its turn asking the human to approve, in prose? */
|
|
253
|
+
function asksApproval(message: { role: string; content?: unknown }): boolean {
|
|
254
|
+
return message.role === "assistant" && APPROVAL_ASK.test(messageText(message.content));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface Offer {
|
|
258
|
+
epoch: number;
|
|
259
|
+
abort: AbortController;
|
|
260
|
+
}
|
|
108
261
|
|
|
109
262
|
export default function designThinkingExtension(pi: ExtensionAPI) {
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
263
|
+
// ── Gate state — the ONLY mutable state; change it via the transitions
|
|
264
|
+
// below, never inline in a handler. ─────────────────────────────────────
|
|
265
|
+
let mode: "off" | "on" = "off";
|
|
266
|
+
// "armed" lets mutating tools through. Approval is NOT consumed by runs:
|
|
267
|
+
// implementing an approved design spans many runs, and re-arming per run
|
|
268
|
+
// only produced approval loops. It is re-locked by relock() when a new user
|
|
269
|
+
// instruction arrives, when a NEW design graph is presented, by /dt deny, or
|
|
270
|
+
// when the mode turns off.
|
|
271
|
+
let approval: "none" | "armed" = "none";
|
|
272
|
+
// A design graph was presented in the CURRENT design cycle. Reset by
|
|
273
|
+
// relock() — i.e. when a new user instruction arrives. Approval is scoped to
|
|
274
|
+
// one human turn: it survives the many runs of implementing an approved
|
|
275
|
+
// design, and drops the moment the user speaks again (a new goal, or a
|
|
276
|
+
// refinement of the current one → re-graph, re-approve).
|
|
277
|
+
let cycleGraphSeen = false;
|
|
278
|
+
// Set by an explicit deny (dialog or /dt deny). Latches until a NEW graph
|
|
279
|
+
// is presented — deny means stop asking.
|
|
280
|
+
let graphDenied = false;
|
|
281
|
+
// Dialog offers. Monotonic epoch; a decision applies only when its epoch
|
|
282
|
+
// matches the live offer and the gate still allows arming.
|
|
283
|
+
let nextEpoch = 1;
|
|
284
|
+
let offer: Offer | undefined;
|
|
285
|
+
// How many messages of the session have already been scanned for VERDICT.
|
|
286
|
+
let scannedMessages = 0;
|
|
287
|
+
|
|
288
|
+
// ── LSP reminder state — always-on, independent of dt mode ───────────────
|
|
289
|
+
// Probed once per session; the suggestion fires at most once per session
|
|
290
|
+
// (reset on session_start). Probing uses pi.getAllTools(), present on both
|
|
291
|
+
// pi 0.84.3 and omp; omp is detected via its pi.logger surface (absent on
|
|
292
|
+
// pi) only to pick the suggestion text.
|
|
293
|
+
let lspProbed = false;
|
|
294
|
+
let lspCapable = false;
|
|
295
|
+
let lspSuggested = false;
|
|
296
|
+
|
|
297
|
+
function isOmp(): boolean {
|
|
298
|
+
return typeof (pi as ExtensionAPI & { logger?: unknown }).logger === "object";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function probeLsp(): void {
|
|
302
|
+
lspProbed = true;
|
|
303
|
+
try {
|
|
304
|
+
const tools: Array<{ name?: string }> =
|
|
305
|
+
(pi as ExtensionAPI & { getAllTools?: () => unknown[] }).getAllTools?.() ?? [];
|
|
306
|
+
lspCapable = tools.some((t) => t?.name === "lsp");
|
|
307
|
+
} catch {
|
|
308
|
+
lspCapable = false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function maybeSuggestLsp(ctx: ExtensionContext): void {
|
|
313
|
+
if (lspSuggested || lspCapable || !ctx.hasUI) return;
|
|
314
|
+
lspSuggested = true;
|
|
315
|
+
ctx.ui.notify(isOmp() ? LSP_SUGGESTION_OMP : LSP_SUGGESTION_PI, "info");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// omp-only pi.logger surface; absent on pi 0.84.3.
|
|
319
|
+
const logger = (pi as ExtensionAPI & { logger?: { warn?: (msg: string) => void } }).logger;
|
|
320
|
+
|
|
321
|
+
// ── Transitions ─────────────────────────────────────────────────────────
|
|
322
|
+
function setMode(on: boolean): void {
|
|
323
|
+
mode = on ? "on" : "off";
|
|
324
|
+
if (!on) {
|
|
325
|
+
approval = "none";
|
|
326
|
+
cycleGraphSeen = false;
|
|
327
|
+
graphDenied = false;
|
|
328
|
+
retractOffer();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Arm the gate. Fails when the mode is off or an approval is already in
|
|
333
|
+
* effect — the caller decides how to surface that. */
|
|
334
|
+
function arm(): boolean {
|
|
335
|
+
if (mode !== "on" || approval !== "none") return false;
|
|
336
|
+
approval = "armed";
|
|
337
|
+
retractOffer();
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Explicit revoke (/dt deny). Latches graph review until a new graph. */
|
|
342
|
+
function revoke(): void {
|
|
343
|
+
approval = "none";
|
|
344
|
+
graphDenied = true;
|
|
345
|
+
retractOffer();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** A new user instruction opens a fresh design decision: re-lock the gate so
|
|
349
|
+
* the agent must present (or update and re-present) the graph and get a new
|
|
350
|
+
* approval before editing. Not called for the extension's own go-ahead /
|
|
351
|
+
* refine messages. */
|
|
352
|
+
function relock(): void {
|
|
353
|
+
if (mode !== "on") return;
|
|
354
|
+
approval = "none";
|
|
355
|
+
cycleGraphSeen = false;
|
|
356
|
+
graphDenied = false;
|
|
357
|
+
retractOffer();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function retractOffer(): void {
|
|
361
|
+
offer?.abort.abort();
|
|
362
|
+
offer = undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** May a dialog decision from `epoch` still be applied? */
|
|
366
|
+
function decisionApplies(epoch: number): boolean {
|
|
367
|
+
return offer?.epoch === epoch && mode === "on" && approval === "none";
|
|
368
|
+
}
|
|
114
369
|
|
|
370
|
+
// ── Session plumbing ────────────────────────────────────────────────────
|
|
115
371
|
function applyStatus(ctx: ExtensionContext) {
|
|
116
|
-
ctx.ui.setStatus(
|
|
117
|
-
"design-thinking",
|
|
118
|
-
enabled ? "design-thinking: on" : undefined,
|
|
119
|
-
);
|
|
372
|
+
ctx.ui.setStatus("design-thinking", mode === "on" ? "design-thinking: on" : undefined);
|
|
120
373
|
}
|
|
121
374
|
|
|
122
375
|
function persist() {
|
|
123
|
-
pi.appendEntry<DtState>(STATE_TYPE, { enabled });
|
|
376
|
+
pi.appendEntry<DtState>(STATE_TYPE, { enabled: mode === "on" });
|
|
124
377
|
}
|
|
125
378
|
|
|
126
379
|
function restore(ctx: ExtensionContext) {
|
|
@@ -134,13 +387,15 @@ export default function designThinkingExtension(pi: ExtensionAPI) {
|
|
|
134
387
|
}
|
|
135
388
|
}
|
|
136
389
|
}
|
|
137
|
-
if (restored !== undefined)
|
|
138
|
-
enabled = restored;
|
|
139
|
-
}
|
|
390
|
+
if (restored !== undefined) setMode(restored);
|
|
140
391
|
}
|
|
141
392
|
|
|
142
|
-
// Restore toggle state when a session starts, loads, or reloads.
|
|
143
393
|
pi.on("session_start", async (_event, ctx) => {
|
|
394
|
+
// A new session gets one fresh LSP probe and suggestion.
|
|
395
|
+
lspProbed = false;
|
|
396
|
+
lspSuggested = false;
|
|
397
|
+
probeLsp();
|
|
398
|
+
maybeSuggestLsp(ctx);
|
|
144
399
|
restore(ctx);
|
|
145
400
|
applyStatus(ctx);
|
|
146
401
|
});
|
|
@@ -149,82 +404,242 @@ export default function designThinkingExtension(pi: ExtensionAPI) {
|
|
|
149
404
|
ctx.ui.setStatus("design-thinking", undefined);
|
|
150
405
|
});
|
|
151
406
|
|
|
152
|
-
// /dt
|
|
153
|
-
// approve|deny arm/revoke file edits; anything else runs as a prompt with mode on.
|
|
407
|
+
// ── /dt command ─────────────────────────────────────────────────────────
|
|
154
408
|
pi.registerCommand("dt", {
|
|
155
|
-
description: "Design Thinking mode: /dt toggles, on|off|status set/query, approve
|
|
409
|
+
description: "Design Thinking mode: /dt toggles, on|off|status set/query, approve asks then implements, deny re-locks; anything else runs as a prompt with mode on",
|
|
156
410
|
handler: async (args, ctx) => {
|
|
157
411
|
const arg = (args ?? "").trim();
|
|
158
412
|
const lower = arg.toLowerCase();
|
|
159
413
|
|
|
160
414
|
if (lower === "status") {
|
|
161
|
-
ctx.ui.notify(`Design Thinking mode: ${
|
|
415
|
+
ctx.ui.notify(`Design Thinking mode: ${mode === "on" ? "on" : "off"}${approval === "armed" ? " (gate armed)" : ""}`, "info");
|
|
162
416
|
return;
|
|
163
417
|
}
|
|
164
418
|
|
|
165
419
|
if (lower === "approve") {
|
|
166
|
-
if (
|
|
420
|
+
if (mode !== "on") {
|
|
167
421
|
ctx.ui.notify("Design Thinking is OFF — nothing to approve. Run /dt on first.", "info");
|
|
168
422
|
return;
|
|
169
423
|
}
|
|
170
|
-
|
|
171
|
-
|
|
424
|
+
// Direct arm: the interactive approve/refine/deny dialog opens
|
|
425
|
+
// automatically after a gated run ends; this command is the
|
|
426
|
+
// headless/scripting path, so no nested dialog here.
|
|
427
|
+
if (!arm()) {
|
|
428
|
+
ctx.ui.notify("Design Thinking: gate is already armed", "info");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
ctx.ui.notify("Design Thinking: gate armed — implementing the approved design without re-presenting the graph", "info");
|
|
432
|
+
// Arming alone leaves the session idle: the agent never learns approval
|
|
433
|
+
// happened and the user sees "nothing happened". Send the go-ahead so
|
|
434
|
+
// the armed run starts now (steer if mid-run).
|
|
435
|
+
await pi.sendUserMessage(GO_AHEAD_MESSAGE, { deliverAs: "steer" });
|
|
172
436
|
return;
|
|
173
437
|
}
|
|
174
438
|
if (lower === "deny") {
|
|
175
|
-
if (
|
|
176
|
-
ctx.ui.notify("Design Thinking: no approval in effect —
|
|
439
|
+
if (approval !== "armed") {
|
|
440
|
+
ctx.ui.notify("Design Thinking: no approval in effect — mutations are already blocked", "info");
|
|
177
441
|
return;
|
|
178
442
|
}
|
|
179
|
-
|
|
180
|
-
ctx.ui.notify("Design Thinking:
|
|
443
|
+
revoke();
|
|
444
|
+
ctx.ui.notify("Design Thinking: mutations blocked again", "info");
|
|
181
445
|
return;
|
|
182
446
|
}
|
|
183
|
-
|
|
184
447
|
// "on"/"off" set explicitly; anything else is a prompt to run in this
|
|
185
448
|
// mode and always turns the mode ON (never toggles it off); bare /dt toggles.
|
|
186
449
|
const prompt = /^(on|off|status)$/.test(lower) ? "" : arg;
|
|
187
|
-
|
|
188
|
-
|
|
450
|
+
const on = lower === "on" || prompt ? true : lower === "off" ? false : mode === "off";
|
|
451
|
+
setMode(on);
|
|
189
452
|
persist();
|
|
190
453
|
applyStatus(ctx);
|
|
191
454
|
ctx.ui.notify(
|
|
192
|
-
|
|
193
|
-
? "Design Thinking ON — plans and reviews will render as
|
|
455
|
+
on
|
|
456
|
+
? "Design Thinking ON — plans and reviews will render as annotated call graphs"
|
|
194
457
|
: "Design Thinking OFF",
|
|
195
458
|
"info",
|
|
196
459
|
);
|
|
197
460
|
|
|
198
461
|
if (prompt) {
|
|
462
|
+
// A prompt via /dt is a new goal: re-lock so the graph comes first
|
|
463
|
+
// (the input handler skips this — sendUserMessage is source "extension").
|
|
464
|
+
relock();
|
|
199
465
|
// deliverAs "steer": sends immediately when idle, queues mid-stream.
|
|
200
466
|
await pi.sendUserMessage(
|
|
201
|
-
`${prompt}\n\n(Design Thinking is ON: before implementing, present the
|
|
467
|
+
`${prompt}\n\n(Design Thinking is ON: before implementing, present the design graph and any clarifying questions, then wait for my go-ahead.)`,
|
|
202
468
|
{ deliverAs: "steer" },
|
|
203
469
|
);
|
|
204
470
|
}
|
|
205
471
|
},
|
|
206
472
|
});
|
|
207
473
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
474
|
+
// ── /research-prompt command ────────────────────────────────────────────
|
|
475
|
+
// r17x's Meta Research Prompt Generator. The template ships in references/;
|
|
476
|
+
// pi prompts cannot read package files, so the command reads it and sends
|
|
477
|
+
// it with the user's message (§6 boundary: file → text, read once here).
|
|
478
|
+
pi.registerCommand("research-prompt", {
|
|
479
|
+
description: "Meta Research Prompt Generator: writes ./research/projects/<NAME>/.<NAME>.research.prompt and prints the run command",
|
|
480
|
+
handler: async (args, ctx) => {
|
|
481
|
+
let generator: string;
|
|
482
|
+
try {
|
|
483
|
+
generator = fs.readFileSync(path.join(REF_DIR, "meta-research.md"), "utf8");
|
|
484
|
+
} catch (err) {
|
|
485
|
+
ctx.ui.notify(`research-prompt: cannot read ${REF_DIR}/meta-research.md (${(err as Error).message})`, "error");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const harness = isOmp() ? "omp" : "pi";
|
|
489
|
+
await pi.sendUserMessage(
|
|
490
|
+
`${generator}\n\n---\n\nYou run inside ${harness}: in step 8 print only the ${harness} command.\n\nUser message:\n${(args ?? "").trim()}`,
|
|
491
|
+
{ deliverAs: "steer" },
|
|
492
|
+
);
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ── Run hooks ───────────────────────────────────────────────────────────
|
|
497
|
+
// A genuine user instruction re-locks the gate: the previous approval covered
|
|
498
|
+
// the previous turn's work only. Our own go-ahead / refine messages arrive as
|
|
499
|
+
// source "extension" and must NOT re-lock.
|
|
500
|
+
pi.on("input", async (event) => {
|
|
501
|
+
if (mode !== "on") return undefined;
|
|
502
|
+
if (event.source === "extension") return undefined;
|
|
503
|
+
relock();
|
|
504
|
+
return undefined;
|
|
214
505
|
});
|
|
215
506
|
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
pi.on("
|
|
219
|
-
|
|
507
|
+
// Compose the system prompt: the distilled block is dt-mode-scoped, the
|
|
508
|
+
// LSP reminder is always-on. Either may be absent.
|
|
509
|
+
pi.on("before_agent_start", async (event) => {
|
|
510
|
+
if (!lspProbed) probeLsp();
|
|
511
|
+
const parts = [event.systemPrompt];
|
|
512
|
+
if (mode === "on") parts.push(DISTILLED + "\n");
|
|
513
|
+
if (lspCapable) parts.push(LSP_REMINDER + "\n");
|
|
514
|
+
if (parts.length === 1) return undefined;
|
|
515
|
+
return { systemPrompt: parts.join("\n\n") };
|
|
220
516
|
});
|
|
221
517
|
|
|
222
|
-
// Hard gate: while enabled, file-edit tools are blocked until the user
|
|
223
|
-
// arms the next run with /dt approve. Prompt-level rules alone proved
|
|
224
|
-
// insufficient — this is the mechanical backstop.
|
|
225
518
|
pi.on("tool_call", async (event) => {
|
|
226
|
-
if (
|
|
519
|
+
if (mode !== "on") return undefined;
|
|
227
520
|
if (!MUTATING_TOOLS[event.toolName]) return undefined;
|
|
521
|
+
if (approval === "armed") return undefined;
|
|
522
|
+
// omp routes MCP calls through `write` (xd://mcp__… device paths); let
|
|
523
|
+
// read-only devices through. qmd devices are pure reads; codegraph
|
|
524
|
+
// only for its read-only actions — init/index/sync/unlock and
|
|
525
|
+
// unparsable args stay blocked.
|
|
526
|
+
if (event.toolName === "write") {
|
|
527
|
+
const input = event.input as { path?: unknown; content?: unknown } | undefined;
|
|
528
|
+
const path = input?.path;
|
|
529
|
+
if (typeof path === "string" && READONLY_MCP_PATH.test(path)) {
|
|
530
|
+
const isCodegraph = path.includes("codegraph_explore");
|
|
531
|
+
let action = "explore"; // codegraph's default action is a read
|
|
532
|
+
if (isCodegraph) {
|
|
533
|
+
try {
|
|
534
|
+
action = (JSON.parse(String(input?.content ?? "{}")) as { action?: string })
|
|
535
|
+
.action ?? "explore";
|
|
536
|
+
} catch {
|
|
537
|
+
action = "init"; // unparsable device args → treat as mutating
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!isCodegraph || READONLY_CODEGRAPH_ACTIONS[action]) return undefined;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Block only — NEVER open the review dialog here. Mid-run, the agent is
|
|
544
|
+
// still streaming, and a UI-up-while-the-agent-acts is the failure this
|
|
545
|
+
// mode exists to prevent. A blocked mutation returns the stop-and-do-not-
|
|
546
|
+
// route-around reason; agent_end offers the dialog (a graph seen earlier
|
|
547
|
+
// this cycle re-offers there too), so the UI appears only after the run
|
|
548
|
+
// ends and the agent has stopped.
|
|
228
549
|
return { block: true, reason: GATE_REASON };
|
|
229
550
|
});
|
|
551
|
+
|
|
552
|
+
// Run-end housekeeping: offer the review dialog when the agent presented a
|
|
553
|
+
// graph this cycle, or ended the turn asking for approval in prose (so the
|
|
554
|
+
// human always gets a button, never a prose-only ask). While an approval
|
|
555
|
+
// holds, edits stay open and no dialog is offered.
|
|
556
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
557
|
+
// Keep the scan cursor in sync even while off, so re-enabling does not
|
|
558
|
+
// rescan or mis-slice history.
|
|
559
|
+
const messages = event.messages;
|
|
560
|
+
if (mode !== "on") {
|
|
561
|
+
scannedMessages = messages.length;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Incremental scan — only messages since the last run end. A long
|
|
566
|
+
// session never re-scans old history.
|
|
567
|
+
const fresh = messages.slice(Math.min(scannedMessages, messages.length));
|
|
568
|
+
scannedMessages = messages.length;
|
|
569
|
+
if (fresh.some(presentsGraph)) {
|
|
570
|
+
cycleGraphSeen = true;
|
|
571
|
+
graphDenied = false;
|
|
572
|
+
approval = "none"; // a presented graph always means: awaiting approval
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (approval === "armed") return;
|
|
576
|
+
if (graphDenied) return;
|
|
577
|
+
if (offer) return; // a dialog is already live
|
|
578
|
+
// Open the dialog on a presented graph, or a prose approval ask.
|
|
579
|
+
if (!cycleGraphSeen && !fresh.some(asksApproval)) return;
|
|
580
|
+
if (!ctx.hasUI) {
|
|
581
|
+
ctx.ui.notify("Design Thinking: review the presented design, then run /dt approve or /dt deny", "info");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
offerReview(ctx);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// ── Review dialog ───────────────────────────────────────────────────────
|
|
588
|
+
// One dialog per offer. Extension-owned AbortSignal: omp dismisses
|
|
589
|
+
// handler-opened dialogs when the handler's 30s budget expires, and a
|
|
590
|
+
// detached promise does not escape that binding — the signal replaces it.
|
|
591
|
+
// If the dialog is dismissed anyway (runtime kill, or the agent's own
|
|
592
|
+
// dialog displacing it), the offer is retracted; the next run end makes a
|
|
593
|
+
// fresh offer. A dismissed dialog is NOT a deny.
|
|
594
|
+
function offerReview(ctx: ExtensionContext): void {
|
|
595
|
+
const epoch = nextEpoch++;
|
|
596
|
+
const abort = new AbortController();
|
|
597
|
+
offer = { epoch, abort };
|
|
598
|
+
|
|
599
|
+
void (async () => {
|
|
600
|
+
const choice = await ctx.ui.select("Review the presented design", [...REVIEW_OPTIONS], {
|
|
601
|
+
signal: abort.signal,
|
|
602
|
+
});
|
|
603
|
+
if (!decisionApplies(epoch)) return; // superseded; discard silently
|
|
604
|
+
|
|
605
|
+
if (choice === undefined) {
|
|
606
|
+
// Dismissed, not decided. File edits stay blocked; a fresh offer
|
|
607
|
+
// opens at the next run end.
|
|
608
|
+
retractOffer();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (choice.startsWith("Deny")) {
|
|
612
|
+
graphDenied = true;
|
|
613
|
+
retractOffer();
|
|
614
|
+
ctx.ui.notify("Design Thinking: mutations blocked — nothing approved", "info");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (choice.startsWith("Approve")) {
|
|
618
|
+
if (!arm()) return; // gate changed while the human was deciding
|
|
619
|
+
ctx.ui.notify("Design Thinking: gate armed — implementing the approved design", "info");
|
|
620
|
+
await pi.sendUserMessage(GO_AHEAD_MESSAGE, { deliverAs: "steer" });
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
// Refine: collect feedback and send it back; the dialog re-opens
|
|
624
|
+
// after the refined plan is presented.
|
|
625
|
+
const feedback = await ctx.ui.input(
|
|
626
|
+
"What should change in the design?",
|
|
627
|
+
"e.g. too complex — drop the cache layer",
|
|
628
|
+
{ signal: abort.signal },
|
|
629
|
+
);
|
|
630
|
+
if (!decisionApplies(epoch)) return;
|
|
631
|
+
if (feedback === undefined || !feedback.trim()) {
|
|
632
|
+
retractOffer();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
retractOffer();
|
|
636
|
+
await pi.sendUserMessage(
|
|
637
|
+
`Refine the presented design (do not implement yet): ${feedback.trim()}\n\n(Design Thinking: update the design graph per this feedback and present it again. Mutations stay blocked.)`,
|
|
638
|
+
{ deliverAs: "steer" },
|
|
639
|
+
);
|
|
640
|
+
})().catch((err: unknown) => {
|
|
641
|
+
if (offer?.epoch === epoch) retractOffer();
|
|
642
|
+
logger?.warn?.(`design-thinking: review dialog failed: ${String(err)}`);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
230
645
|
}
|