@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 +7 -1
- package/extensions/design-thinking.ts +457 -55
- package/package.json +2 -2
- package/prompts/cg-map.md +2 -3
- package/prompts/cg-plan.md +2 -3
- package/prompts/cg-review.md +2 -3
- package/prompts/cg.md +2 -3
- package/skills/design-graph/SKILL.md +1 -0
- package/skills/design-method/SKILL.md +1 -0
- package/skills/graph-protocol/SKILL.md +1 -0
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.
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
|
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
|
|
68
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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)
|
|
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
|
-
|
|
106
|
-
//
|
|
107
|
-
let
|
|
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
|
|
401
|
+
// ── /dt command ─────────────────────────────────────────────────────────
|
|
147
402
|
pi.registerCommand("dt", {
|
|
148
|
-
description: "Design Thinking mode: /dt toggles, on|off|status set/query, approve
|
|
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: ${
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
return
|
|
203
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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
|
|
40
|
-
|
|
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).
|
package/prompts/cg-plan.md
CHANGED
|
@@ -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
|
|
64
|
-
|
|
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).
|
package/prompts/cg-review.md
CHANGED
|
@@ -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
|
|
68
|
-
|
|
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,
|
|
49
|
-
|
|
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).
|
|
@@ -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
|