@widnyana/design-thinking 0.0.6 → 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,7 +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. |
64
- | `/dt approve\|deny` | Arm/revoke file edits for the next agent run — run `approve` after a presented graph to let the implementation run edit, `deny` to re-lock. |
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. |
65
70
  | `/dt <prompt>` | Turn the mode **on** (never off), then run `<prompt>` under it — graph and clarifying questions first, implementation after your go-ahead. |
66
71
  | `/cg <module \| task>` | Generate a call graph: extract the graph existing code implements, or sketch one for a task. |
67
72
  | `/cg-plan <task>` | Design before code — full Design Graph, then implement to match it. |
@@ -10,14 +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 approve|deny arm/revoke file edits for the next agent run
19
- * /dt <prompt> turn mode ON (never off), then run <prompt> as a user
20
- * 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.
21
50
  *
22
51
  * Stack-agnostic: no language detection, no auto-enable. Adapt the vocabulary
23
52
  * to whatever stack the project uses; never the discipline.
@@ -62,13 +91,20 @@ Pipeline: shapes → graph(A) → cardinality → E(⟳retry ↯escape ☠die)
62
91
  🔒 parse at boundaries → ⛈ orthogonal behavior → scope → test-R → verdict.
63
92
 
64
93
  Rules:
65
- - 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,
66
95
  refactor — the model does NOT get to reclassify the request): render the
67
96
  Design Graph in Graph Protocol form and any clarifying questions FIRST;
68
- ZERO file edits until the user approves the design. Reading/exploring code
69
- first is fine; the FIRST edit is not allowed before the graph. Once the
70
- user runs /dt approve, edits are armed for the NEXT run: implement the
71
- approved design without re-presenting 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).
72
108
  - Design, plan, and review answers must render a Design Graph — fixed
73
109
  sections: PROBLEM, SHAPES, GRAPH, CARDINALITY, BOUNDARIES, BEHAVIOR, SCOPE,
74
110
  TEST LAYERS, VERDICT.
@@ -81,46 +117,257 @@ Full specs — read these when a section needs detail:
81
117
  - ${REF_DIR}/protocol.md (Graph Protocol: notation spec + worked example)
82
118
  - ${REF_DIR}/method.md (the §1–§10 method, generalized)
83
119
  - ${REF_DIR}/design-graph.md (the artifact spec + completeness checklist)
84
- Skills load BY NAME into the agent (/skill:<name>); the files above are named
85
- by stem, NOT by skill name. Exact mapping:
86
- /skill:graph-protocol → protocol.md /skill:design-method → method.md
87
- /skill:design-graph → design-graph.md
88
- The extension blocks write/edit tools mechanically until the user runs
89
- /dt approve — it arms the NEXT run, cleared when that run ends. Re-approve
90
- per implementation round. 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.
91
131
  NEVER read references/<skill-name>.md — no such files exist.
92
132
  `.trim();
93
133
 
94
- // pi's file-edit tools — the mechanical gate's scope ("file edits"). Bash
95
- // stays open for read-only exploration. apply_patch/multiedit are Claude Code
96
- // tool names that never fire in pi; keep this list to real pi mutators.
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).
97
143
  const MUTATING_TOOLS: Record<string, true> = {
98
144
  write: true,
99
145
  edit: 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,
100
171
  };
101
172
 
102
173
  interface DtState {
103
174
  enabled: boolean;
104
175
  }
105
176
 
177
+
178
+
106
179
  const GATE_REASON =
107
- "Design Thinking mode is ON: present the Design Graph (Graph Protocol) and wait for user approval before file edits. The user arms the NEXT run with /dt approve; the approval clears when that run ends.";
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
+ }
108
255
 
109
256
  export default function designThinkingExtension(pi: ExtensionAPI) {
110
- let enabled = false;
111
- // Armed by /dt approve for the NEXT agent run. Cleared when that run ends
112
- // (agent_end), by /dt deny, or when the mode turns off. Not persisted.
113
- let editsApproved = 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
+ }
114
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
+ }
363
+
364
+ // ── Session plumbing ────────────────────────────────────────────────────
115
365
  function applyStatus(ctx: ExtensionContext) {
116
- ctx.ui.setStatus(
117
- "design-thinking",
118
- enabled ? "design-thinking: on" : undefined,
119
- );
366
+ ctx.ui.setStatus("design-thinking", mode === "on" ? "design-thinking: on" : undefined);
120
367
  }
121
368
 
122
369
  function persist() {
123
- pi.appendEntry<DtState>(STATE_TYPE, { enabled });
370
+ pi.appendEntry<DtState>(STATE_TYPE, { enabled: mode === "on" });
124
371
  }
125
372
 
126
373
  function restore(ctx: ExtensionContext) {
@@ -134,13 +381,15 @@ export default function designThinkingExtension(pi: ExtensionAPI) {
134
381
  }
135
382
  }
136
383
  }
137
- if (restored !== undefined) {
138
- enabled = restored;
139
- }
384
+ if (restored !== undefined) setMode(restored);
140
385
  }
141
386
 
142
- // Restore toggle state when a session starts, loads, or reloads.
143
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);
144
393
  restore(ctx);
145
394
  applyStatus(ctx);
146
395
  });
@@ -149,53 +398,64 @@ export default function designThinkingExtension(pi: ExtensionAPI) {
149
398
  ctx.ui.setStatus("design-thinking", undefined);
150
399
  });
151
400
 
152
- // /dt — the Design Thinking tool. Toggles mode; on|off|status set/query;
153
- // approve|deny arm/revoke file edits; anything else runs as a prompt with mode on.
401
+ // ── /dt command ─────────────────────────────────────────────────────────
154
402
  pi.registerCommand("dt", {
155
- description: "Design Thinking mode: /dt toggles, on|off|status set/query, approve arms the next run for file edits, deny revokes; 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",
156
404
  handler: async (args, ctx) => {
157
405
  const arg = (args ?? "").trim();
158
406
  const lower = arg.toLowerCase();
159
407
 
160
408
  if (lower === "status") {
161
- ctx.ui.notify(`Design Thinking mode: ${enabled ? "on" : "off"}${editsApproved ? " (file edits armed for the next run)" : ""}`, "info");
409
+ ctx.ui.notify(`Design Thinking mode: ${mode === "on" ? "on" : "off"}${approval === "armed" ? " (gate armed)" : ""}`, "info");
162
410
  return;
163
411
  }
164
412
 
165
413
  if (lower === "approve") {
166
- if (!enabled) {
414
+ if (mode !== "on") {
167
415
  ctx.ui.notify("Design Thinking is OFF — nothing to approve. Run /dt on first.", "info");
168
416
  return;
169
417
  }
170
- editsApproved = true;
171
- ctx.ui.notify("Design Thinking: file edits armed (current or next run) — it may implement the approved design without re-presenting the graph", "info");
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" });
172
430
  return;
173
431
  }
174
432
  if (lower === "deny") {
175
- if (!editsApproved) {
176
- ctx.ui.notify("Design Thinking: no approval in effect — file edits are already blocked", "info");
433
+ if (approval !== "armed") {
434
+ ctx.ui.notify("Design Thinking: no approval in effect — mutations are already blocked", "info");
177
435
  return;
178
436
  }
179
- editsApproved = false;
180
- ctx.ui.notify("Design Thinking: file edits blocked again", "info");
437
+ revoke();
438
+ ctx.ui.notify("Design Thinking: mutations blocked again", "info");
181
439
  return;
182
440
  }
183
-
184
441
  // "on"/"off" set explicitly; anything else is a prompt to run in this
185
442
  // mode and always turns the mode ON (never toggles it off); bare /dt toggles.
186
443
  const prompt = /^(on|off|status)$/.test(lower) ? "" : arg;
187
- enabled = lower === "on" || prompt ? true : lower === "off" ? false : !enabled;
188
- if (!enabled) editsApproved = false;
444
+ const on = lower === "on" || prompt ? true : lower === "off" ? false : mode === "off";
445
+ setMode(on);
189
446
  persist();
190
447
  applyStatus(ctx);
191
448
  ctx.ui.notify(
192
- enabled
449
+ on
193
450
  ? "Design Thinking ON — plans and reviews will render as Design Graphs (Graph Protocol)"
194
451
  : "Design Thinking OFF",
195
452
  "info",
196
453
  );
197
454
 
198
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();
199
459
  // deliverAs "steer": sends immediately when idle, queues mid-stream.
200
460
  await pi.sendUserMessage(
201
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.)`,
@@ -205,26 +465,153 @@ export default function designThinkingExtension(pi: ExtensionAPI) {
205
465
  },
206
466
  });
207
467
 
208
- // While active, append the distilled block to the system prompt every run.
209
- pi.on("before_agent_start", async (event) => {
210
- if (!enabled) return undefined;
211
- return {
212
- systemPrompt: event.systemPrompt + "\n\n" + DISTILLED + "\n",
213
- };
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;
214
477
  });
215
478
 
216
- // An approved run consumes the approval: clear it when the run ends so
217
- // later runs are gated again until the user re-approves with /dt approve.
218
- pi.on("agent_end", async () => {
219
- editsApproved = false;
479
+ // Compose the system prompt: the distilled block is dt-mode-scoped, the
480
+ // LSP reminder is always-on. Either may be absent.
481
+ pi.on("before_agent_start", async (event) => {
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") };
220
488
  });
221
489
 
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
490
  pi.on("tool_call", async (event) => {
226
- if (!enabled || editsApproved) return undefined;
491
+ if (mode !== "on") return undefined;
227
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.
228
521
  return { block: true, reason: GATE_REASON };
229
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
+ }
230
617
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widnyana/design-thinking",
3
- "version": "0.0.6",
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