@widnyana/design-thinking 0.0.5 → 0.0.20

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