selfware 0.6.7

Your personal AI workshop — software you own, software that lasts
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
{
  "component": "interview",
  "tier": "tooling",
  "loop_stage": "perceive",
  "summary": "The interview component runs a pre-task structured questionnaire that scopes the work before the agent loop starts. run_interview asks language, framework, project type, testing preference, output location, and scope questions (analyze_task heuristics and detect_existing_project pre-fill defaults), collects answers into an InterviewSession, converts them to a validated InterviewContext, and serializes them via to_system_prompt_section into the agent's system prompt. On the loop it is a boundary 'perceive'/'control' node: it perceives operator intent up front and constrains the loop's plan and budget before the first Planning transition, and it is skippable at any point via Esc.",
  "loop_objects": ["InterviewQuestion", "InterviewSession", "InterviewContext", "ProjectType", "TestingPreference", "ProjectScope", "QuestionOption", "TaskHints", "Plan", "Budget"],
  "context_basis": "Recommendations were formed by reading src/interview.rs in the context of the full engine's ~600k-token budget framing, so each move treats the interview as a cheap pre-loop scoping pass that shrinks the plan's search space and constrains budget before any tokens are spent iterating.",
  "examples": [
    {
      "id": "interview-01",
      "title": "Scope the task before the loop starts",
      "loop_stage": "perceive",
      "pattern": "perceive-before-plan",
      "intent": "Capture operator intent as structured context so the first plan is grounded, not guessed.",
      "how_it_shapes_the_loop": "run_interview runs before the Planning transition and produces an InterviewContext; the loop enters its first iteration already knowing language, scope, and constraints instead of inferring them from the raw task string.",
      "loop_objects_touched": ["InterviewContext", "InterviewSession"],
      "wiring": {
        "inputs_from": ["raw task string", "cwd"],
        "outputs_to": ["system prompt", "planner"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tap the interview node at the loop's entrance to begin the pre-task questionnaire.",
        "visual": "The node sits as a gate before the Planning node, glowing amber until answered, then green as context is committed."
      },
      "mini_scenario": "Before planning, the interview asks six questions; the answers become the InterviewContext the planner reads on iteration one.",
      "pitfall": "The interview must run before the first plan — asking mid-loop wastes the scoping value, since the plan is already committed."
    },
    {
      "id": "interview-02",
      "title": "Analyze the task to pre-fill defaults",
      "loop_stage": "perceive",
      "pattern": "heuristic-priming",
      "intent": "Reduce operator effort by inferring likely answers from the task text.",
      "how_it_shapes_the_loop": "analyze_task returns TaskHints (suggests_web_api, suggests_cli, mentioned_languages, ...); the loop pre-selects defaults so fewer questions need answering, tightening scope faster.",
      "loop_objects_touched": ["TaskHints", "InterviewQuestion"],
      "wiring": {
        "inputs_from": ["task string keywords"],
        "outputs_to": ["question defaults", "InterviewSession"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-press a question node to see the TaskHints that pre-selected its default.",
        "visual": "The inferred default option is pre-highlighted with a small 'from task' badge explaining the guess."
      },
      "mini_scenario": "The task says 'build a REST API in Rust'; analyze_task flags suggests_web_api and mentions Rust, pre-filling both questions.",
      "pitfall": "Heuristics are hints, not answers — always let the operator override the pre-fill; a wrong guess silently mis-scopes the loop."
    },
    {
      "id": "interview-03",
      "title": "Skip language when the project is known",
      "loop_stage": "control",
      "pattern": "conditional-question",
      "intent": "Avoid asking what the environment already answers.",
      "how_it_shapes_the_loop": "detect_existing_project inspects for Cargo.toml, package.json, pyproject.toml, or go.mod; when a language is detected the language question is skipped, so the loop scopes without redundant prompting.",
      "loop_objects_touched": ["InterviewQuestion", "InterviewSession"],
      "wiring": {
        "inputs_from": ["cwd project markers"],
        "outputs_to": ["skipped question", "InterviewSession.language"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flick past the language question node when the detected project auto-answers it.",
        "visual": "The language node renders pre-filled and dimmed with a 'detected: Rust' chip, non-interactive."
      },
      "mini_scenario": "The cwd holds Cargo.toml; the interview detects Rust, skips the language question, and moves straight to framework.",
      "pitfall": "Detection keys on marker files — an unusual layout without them still prompts, so don't assume detection always fires."
    },
    {
      "id": "interview-04",
      "title": "Offer language-aware framework choices",
      "loop_stage": "perceive",
      "pattern": "contextual-options",
      "intent": "Present only frameworks that fit the chosen language.",
      "how_it_shapes_the_loop": "The framework question's QuestionOptions depend on the selected language (axum/actix/tokio for Rust, FastAPI/Flask/Django for Python, ...); the loop's plan inherits a coherent language+framework pair.",
      "loop_objects_touched": ["InterviewQuestion", "QuestionOption", "InterviewSession"],
      "wiring": {
        "inputs_from": ["selected language"],
        "outputs_to": ["InterviewSession.framework"]
      },
      "touch_interaction": {
        "gesture": "spread",
        "canvas_action": "Spread the framework node to fan out the options valid for the selected language.",
        "visual": "Only language-compatible framework chips appear; incompatible ones are absent rather than greyed."
      },
      "mini_scenario": "Language is Python, so the framework question offers FastAPI, Flask, and Django rather than Rust web frameworks.",
      "pitfall": "Don't offer cross-language frameworks — an axum option under Python would let the operator scope an impossible plan."
    },
    {
      "id": "interview-05",
      "title": "Classify the project type",
      "loop_stage": "perceive",
      "pattern": "type-classification",
      "intent": "Fix the shape of the deliverable so the plan targets the right structure.",
      "how_it_shapes_the_loop": "The project-type question maps to the ProjectType enum (CliTool, WebApi, FrontendUi, Library, FullStack, Script, Other); the loop plans a CLI scaffold vs a web service based on this single choice.",
      "loop_objects_touched": ["InterviewQuestion", "ProjectType", "InterviewSession"],
      "wiring": {
        "inputs_from": ["project-type selection", "TaskHints"],
        "outputs_to": ["InterviewContext.project_type", "planner scaffold"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tap a project-type chip to lock the deliverable shape for the loop.",
        "visual": "Selected type highlights and stamps a type icon (terminal, globe, book) on the interview node."
      },
      "mini_scenario": "The operator picks WebApi; the plan scaffolds routes and handlers rather than a library crate.",
      "pitfall": "ProjectType::Other(String) carries a freeform label — the plan must read the string, not assume a built-in variant."
    },
    {
      "id": "interview-06",
      "title": "Choose a testing discipline",
      "loop_stage": "control",
      "pattern": "verify-policy",
      "intent": "Decide up front how much the loop's verify stage will invest in tests.",
      "how_it_shapes_the_loop": "The testing question maps to TestingPreference (Tdd, TestsAfter, Minimal, None); this sets how aggressively the loop's verify stage writes and runs tests each iteration.",
      "loop_objects_touched": ["InterviewQuestion", "TestingPreference", "InterviewContext"],
      "wiring": {
        "inputs_from": ["testing-preference selection"],
        "outputs_to": ["verify-stage policy", "system prompt"]
      },
      "touch_interaction": {
        "gesture": "drag",
        "canvas_action": "Drag the testing dial from None through Minimal, TestsAfter, to Tdd to set verify intensity.",
        "visual": "A four-notch dial; the verify node downstream brightens as the notch climbs toward Tdd."
      },
      "mini_scenario": "The operator selects Tdd; the loop writes a failing test before each implementation act throughout the run.",
      "pitfall": "TestingPreference::None means the verify stage skips test authoring — don't silently add tests the operator opted out of."
    },
    {
      "id": "interview-07",
      "title": "Bound expected effort with scope",
      "loop_stage": "control",
      "pattern": "budget-anchor",
      "intent": "Anchor the loop's budget expectation to a size class before it starts iterating.",
      "how_it_shapes_the_loop": "The scope question maps to ProjectScope (Quick <100, Small 100-500, Medium 500-2000, Large 2000+ lines); the loop uses this to right-size its budget and iteration count expectations.",
      "loop_objects_touched": ["InterviewQuestion", "ProjectScope", "Budget"],
      "wiring": {
        "inputs_from": ["scope selection", "TaskHints"],
        "outputs_to": ["Budget sizing", "planner iteration estimate"]
      },
      "touch_interaction": {
        "gesture": "pinch",
        "canvas_action": "Pinch the scope node to compress or expand the expected line-count band.",
        "visual": "A size gauge spans Quick to Large; the loop's budget ring resizes to match the chosen band."
      },
      "mini_scenario": "The operator picks Small; the loop expects a 100-500 line deliverable and sizes its budget for a short run.",
      "pitfall": "Scope is an expectation, not a hard cap — don't let the loop refuse legitimate work that overruns the estimated band."
    },
    {
      "id": "interview-08",
      "title": "Ask a freeform sub-question for output location",
      "loop_stage": "perceive",
      "pattern": "conditional-freeform",
      "intent": "Capture a custom subdirectory when the operator wants a new location.",
      "how_it_shapes_the_loop": "Selecting 'new subdirectory' triggers ask_freeform to collect the path; the loop's file-writing acts are then scoped to that directory instead of the cwd.",
      "loop_objects_touched": ["InterviewQuestion", "InterviewSession"],
      "wiring": {
        "inputs_from": ["output-location choice", "freeform path input"],
        "outputs_to": ["InterviewContext.output_dir"]
      },
      "touch_interaction": {
        "gesture": "double-tap",
        "canvas_action": "Double-tap the 'new subdirectory' option to open its freeform text field on the canvas.",
        "visual": "A text input slides out beneath the option; the entered path appears as a scoped-directory chip."
      },
      "mini_scenario": "The operator chooses a new subdir and types 'service/'; the loop writes all generated files under service/.",
      "pitfall": "The output-location question is skipped inside an existing project — don't force a new-dir prompt when work belongs in place."
    },
    {
      "id": "interview-09",
      "title": "Serialize answers into the system prompt",
      "loop_stage": "control",
      "pattern": "context-injection",
      "intent": "Deliver the scoped context to the loop in the form it actually reads.",
      "how_it_shapes_the_loop": "to_system_prompt_section formats the InterviewContext as a '## Interview Context (user-specified preferences)' markdown block injected into the agent's system prompt, so every iteration sees the constraints.",
      "loop_objects_touched": ["InterviewContext"],
      "wiring": {
        "inputs_from": ["InterviewContext"],
        "outputs_to": ["agent system prompt"]
      },
      "touch_interaction": {
        "gesture": "draw-connection",
        "canvas_action": "Draw a connection from the interview node into the system-prompt node to inject the context block.",
        "visual": "A context ribbon flows along the edge and docks as a labeled section inside the prompt node."
      },
      "mini_scenario": "The context serializes to a markdown section listing Rust, axum, WebApi, Tdd; the planner reads it on every turn.",
      "pitfall": "Inject the section once, before the loop — regenerating it mid-run risks contradicting decisions the loop already made."
    },
    {
      "id": "interview-10",
      "title": "Detect an empty interview and fall back to defaults",
      "loop_stage": "control",
      "pattern": "graceful-default",
      "intent": "Let the loop proceed on best judgment when the operator answered nothing.",
      "how_it_shapes_the_loop": "is_empty checks whether every field is None/empty; if the operator skipped everything, the caller omits the context section and the loop plans on its own defaults rather than an empty scaffold.",
      "loop_objects_touched": ["InterviewContext", "InterviewSession"],
      "wiring": {
        "inputs_from": ["fully-skipped InterviewSession"],
        "outputs_to": ["default planning path"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flick past the entire interview to skip it; the node collapses to a 'defaults' marker.",
        "visual": "The interview node greys out with an 'empty — using best judgment' badge, and no context edge is drawn."
      },
      "mini_scenario": "The operator hits Esc at the first question; is_empty is true, no section is injected, and the loop uses its own defaults.",
      "pitfall": "An empty context is valid — don't inject a blank interview section, which would tell the loop the operator wants nothing rather than left it open."
    },
    {
      "id": "interview-11",
      "title": "Skip the whole interview with Esc",
      "loop_stage": "control",
      "pattern": "opt-out",
      "intent": "Give the operator an instant escape from scoping.",
      "how_it_shapes_the_loop": "read_line_or_esc detects Esc in TTY mode and returns LineInput::Esc; pressing it at any question stops the questionnaire and returns whatever was collected, so the loop starts immediately.",
      "loop_objects_touched": ["InterviewQuestion", "InterviewSession"],
      "wiring": {
        "inputs_from": ["Esc keypress"],
        "outputs_to": ["partial InterviewSession", "loop start"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flick down anywhere on the questionnaire to abort remaining questions.",
        "visual": "Remaining question nodes fade and the loop's Planning gate unlocks immediately."
      },
      "mini_scenario": "After answering language and framework, the operator hits Esc; those two answers persist and the loop begins.",
      "pitfall": "Esc keeps partial answers, it doesn't discard them — don't treat a mid-interview Esc as an empty context."
    },
    {
      "id": "interview-12",
      "title": "Present numbered multiple-choice questions",
      "loop_stage": "perceive",
      "pattern": "structured-choice",
      "intent": "Collect an unambiguous answer via a keyed menu.",
      "how_it_shapes_the_loop": "ask_multiple_choice renders QuestionOptions with char keys and reads a TTY-aware selection, returning None on Esc or the chosen label; the loop receives a clean enum-mappable answer rather than free text.",
      "loop_objects_touched": ["InterviewQuestion", "QuestionOption"],
      "wiring": {
        "inputs_from": ["numbered option keys"],
        "outputs_to": ["selected label", "enum parser"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tap a numbered option chip to select it.",
        "visual": "Each option is a keyed chip (a, b, c); the tapped chip fills solid and the rest dim."
      },
      "mini_scenario": "The scope question shows keys a-d; the operator taps 'b' for Small and ask_multiple_choice returns its label.",
      "pitfall": "A None return means skipped, not a default choice — the caller must distinguish 'no answer' from 'first option'."
    },
    {
      "id": "interview-13",
      "title": "Handle non-interactive input gracefully",
      "loop_stage": "foundation",
      "pattern": "tty-fallback",
      "intent": "Keep the interview working when stdin is piped, not a terminal.",
      "how_it_shapes_the_loop": "read_line_or_esc falls back to a plain read_line in non-interactive mode (no crossterm raw mode); the loop can still be scoped from a script without a TTY, or cleanly skip.",
      "loop_objects_touched": ["InterviewQuestion", "InterviewSession"],
      "wiring": {
        "inputs_from": ["piped stdin"],
        "outputs_to": ["collected answers or skip"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-press the interview node to see whether it's in interactive or piped-input mode.",
        "visual": "A mode badge shows a keyboard glyph for TTY or a pipe glyph for scripted input."
      },
      "mini_scenario": "A CI job pipes answers into selfware init; the fallback read_line consumes them line by line without raw-mode.",
      "pitfall": "Esc detection needs raw TTY mode — in piped mode there's no Esc, so scripts must supply explicit skip lines instead."
    },
    {
      "id": "interview-14",
      "title": "Map answers into typed enums",
      "loop_stage": "reason",
      "pattern": "parse-to-domain",
      "intent": "Turn free labels into the domain enums the loop reasons over.",
      "how_it_shapes_the_loop": "parse_project_type, parse_testing_preference, and parse_scope convert selected labels into ProjectType/TestingPreference/ProjectScope; the loop reasons over typed values, not strings.",
      "loop_objects_touched": ["ProjectType", "TestingPreference", "ProjectScope", "InterviewContext"],
      "wiring": {
        "inputs_from": ["selected labels"],
        "outputs_to": ["InterviewContext typed fields"]
      },
      "touch_interaction": {
        "gesture": "double-tap",
        "canvas_action": "Double-tap an answered question to see its label resolve into a typed enum value.",
        "visual": "The label chip morphs into a typed badge (e.g. 'Web API' -> ProjectType::WebApi)."
      },
      "mini_scenario": "The 'Full TDD' label parses to TestingPreference::Tdd, which the verify stage reads directly.",
      "pitfall": "An unrecognized custom label falls through to Other(String) — the loop must handle the freeform case, not panic on an unknown enum."
    },
    {
      "id": "interview-15",
      "title": "Convert a session into a validated context",
      "loop_stage": "control",
      "pattern": "validate-before-use",
      "intent": "Promote raw collected answers into a context the loop can trust.",
      "how_it_shapes_the_loop": "session_to_context turns the raw InterviewSession (optional fields) into an InterviewContext carrying the task and validated fields; the loop consumes a coherent, task-bound context rather than loose answers.",
      "loop_objects_touched": ["InterviewSession", "InterviewContext"],
      "wiring": {
        "inputs_from": ["InterviewSession"],
        "outputs_to": ["InterviewContext"]
      },
      "touch_interaction": {
        "gesture": "pinch",
        "canvas_action": "Pinch the session node to consolidate its scattered answers into a single context card.",
        "visual": "Loose answer chips draw together into one bound InterviewContext card stamped with the task."
      },
      "mini_scenario": "session_to_context folds the answers plus the original task into one InterviewContext the planner reads.",
      "pitfall": "The context binds the task alongside the answers — dropping the task on conversion strips the answers of what they scope."
    },
    {
      "id": "interview-16",
      "title": "Infer scope for script-like tasks",
      "loop_stage": "reason",
      "pattern": "default-inference",
      "intent": "Pre-answer scope when the task obviously implies a small script.",
      "how_it_shapes_the_loop": "infer_scope_default returns 'a' (Quick) when TaskHints.suggests_script, else None; the loop's budget starts small for throwaway scripts without asking.",
      "loop_objects_touched": ["TaskHints", "ProjectScope", "InterviewQuestion"],
      "wiring": {
        "inputs_from": ["TaskHints.suggests_script"],
        "outputs_to": ["scope question default"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-press the scope node to see why Quick was pre-selected for a script task.",
        "visual": "The Quick notch is pre-lit with a 'script detected' tag explaining the inference."
      },
      "mini_scenario": "The task 'write a quick script to rename files' pre-selects Quick scope, so the loop budgets for a short run.",
      "pitfall": "Inference returns None when unsure — don't fabricate a scope default; an absent default correctly leaves the question open."
    },
    {
      "id": "interview-17",
      "title": "Skip framework for script or unknown language",
      "loop_stage": "control",
      "pattern": "conditional-question",
      "intent": "Avoid asking for a framework where none applies.",
      "how_it_shapes_the_loop": "The framework question is skipped when the task suggests a script or no language is known; the loop scopes without a meaningless framework choice.",
      "loop_objects_touched": ["InterviewQuestion", "TaskHints"],
      "wiring": {
        "inputs_from": ["TaskHints", "known language state"],
        "outputs_to": ["skipped framework question"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flick past the framework node when the task is a bare script.",
        "visual": "The framework node collapses with a 'not applicable' badge and no options shown."
      },
      "mini_scenario": "The task is a shell one-liner script; the framework question is skipped entirely and the loop proceeds.",
      "pitfall": "Skipping framework is correct for scripts — don't force a framework onto a task that has no framework concept."
    },
    {
      "id": "interview-18",
      "title": "Accumulate extra notes as freeform scope",
      "loop_stage": "perceive",
      "pattern": "open-capture",
      "intent": "Let the operator add constraints the fixed questions don't cover.",
      "how_it_shapes_the_loop": "InterviewSession.extra_notes (Vec<String>) collects freeform lines; these flow into the system prompt so the loop honors constraints outside the standard question set.",
      "loop_objects_touched": ["InterviewSession", "InterviewContext"],
      "wiring": {
        "inputs_from": ["freeform note input"],
        "outputs_to": ["InterviewContext.extra_notes", "system prompt"]
      },
      "touch_interaction": {
        "gesture": "draw-connection",
        "canvas_action": "Draw a line from a sticky-note bubble to the interview node to append an extra note.",
        "visual": "Each note appears as a stacked sticky chip on the interview node, order preserved."
      },
      "mini_scenario": "The operator adds 'must be dependency-free'; the note lands in extra_notes and constrains the loop's plan.",
      "pitfall": "Extra notes are unstructured — the loop must actually read them; treating them as decoration lets real constraints slip."
    },
    {
      "id": "interview-19",
      "title": "Infer project type from task signals",
      "loop_stage": "reason",
      "pattern": "default-inference",
      "intent": "Pre-select the likely project type to shorten the questionnaire.",
      "how_it_shapes_the_loop": "infer_project_type_default returns a char key from TaskHints ('b' web_api, 'a' cli, 'c' frontend, 'd' library, 'f' script); the loop pre-fills the type question, needing only confirmation.",
      "loop_objects_touched": ["TaskHints", "ProjectType", "InterviewQuestion"],
      "wiring": {
        "inputs_from": ["TaskHints signals"],
        "outputs_to": ["project-type default"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-press the type node to see which task signal chose the default key.",
        "visual": "The inferred type chip is pre-lit with the matching signal name shown beneath it."
      },
      "mini_scenario": "The task mentions 'CLI tool'; infer_project_type_default returns 'a' and pre-selects CliTool for confirmation.",
      "pitfall": "The inference maps to a char key, not a final enum — the operator's confirmation still runs through parse_project_type."
    },
    {
      "id": "interview-20",
      "title": "Distinguish TypeScript from JavaScript on detection",
      "loop_stage": "perceive",
      "pattern": "precise-detection",
      "intent": "Detect the exact language variant so the plan targets the right toolchain.",
      "how_it_shapes_the_loop": "detect_existing_project checks tsconfig.json vs package.json alone to tell TypeScript from JavaScript (and pyproject.toml/setup.py/requirements.txt for Python); the loop scopes to the precise ecosystem.",
      "loop_objects_touched": ["InterviewQuestion", "InterviewSession"],
      "wiring": {
        "inputs_from": ["project marker files"],
        "outputs_to": ["detected language", "skipped language question"]
      },
      "touch_interaction": {
        "gesture": "double-tap",
        "canvas_action": "Double-tap the detected-language chip to see which marker file resolved the variant.",
        "visual": "The chip shows 'TypeScript' with a tsconfig.json footnote, distinct from a plain package.json JS detection."
      },
      "mini_scenario": "The cwd has both package.json and tsconfig.json; detection reports TypeScript, so the plan targets ts tooling.",
      "pitfall": "A package.json without tsconfig.json is JavaScript, not TypeScript — conflating them scopes the loop to the wrong toolchain."
    }
  ]
}