claude-mcp-workflow 0.1.8 → 0.1.9

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.
Files changed (56) hide show
  1. package/.claude-plugin/plugin.json +4 -2
  2. package/README.md +37 -0
  3. package/build/engine.d.ts +13 -1
  4. package/build/engine.d.ts.map +1 -1
  5. package/build/engine.js +41 -2
  6. package/build/engine.js.map +1 -1
  7. package/build/types.d.ts +20 -20
  8. package/build/types.js +1 -1
  9. package/build/types.js.map +1 -1
  10. package/hooks/hooks.json +4 -2
  11. package/hooks/workflow-cleanup.sh +8 -4
  12. package/hooks/workflow-start.sh +60 -32
  13. package/package.json +1 -1
  14. package/templates/bug-fix.yaml +98 -16
  15. package/templates/code-review.yaml +30 -12
  16. package/templates/coding.yaml +49 -4
  17. package/templates/debugging.yaml +39 -14
  18. package/templates/explore.yaml +48 -14
  19. package/templates/file-code.yaml +13 -4
  20. package/templates/file-review.yaml +25 -6
  21. package/templates/github-init.yaml +24 -8
  22. package/templates/investigate.yaml +13 -4
  23. package/templates/master.yaml +16 -13
  24. package/templates/new-feature.yaml +24 -26
  25. package/templates/planning.yaml +34 -7
  26. package/templates/reflection.yaml +11 -4
  27. package/templates/review-push.yaml +26 -7
  28. package/templates/skills/architecture/SKILL.md +131 -1
  29. package/templates/skills/aws-lambda/SKILL.md +9 -9
  30. package/templates/skills/browser-verify/SKILL.md +58 -0
  31. package/templates/skills/build-cmake/SKILL.md +24 -8
  32. package/templates/skills/ci-github-actions/SKILL.md +34 -18
  33. package/templates/skills/claude-code-config/SKILL.md +41 -19
  34. package/templates/skills/cleanup/SKILL.md +57 -0
  35. package/templates/skills/coding-skill-selector/SKILL.md +2 -1
  36. package/templates/skills/debug-bridge-scaffold/SKILL.md +98 -0
  37. package/templates/skills/domain-gamedev/SKILL.md +56 -6
  38. package/templates/skills/domain-pixi/SKILL.md +256 -7
  39. package/templates/skills/domain-reid/SKILL.md +39 -5
  40. package/templates/skills/domain-yolo/SKILL.md +18 -7
  41. package/templates/skills/ide-zed/SKILL.md +23 -0
  42. package/templates/skills/lang-as3/SKILL.md +7 -5
  43. package/templates/skills/lang-haxe/SKILL.md +538 -19
  44. package/templates/skills/lang-python/SKILL.md +6 -2
  45. package/templates/skills/math/SKILL.md +64 -2
  46. package/templates/skills/mcp-setup/SKILL.md +14 -2
  47. package/templates/skills/preferences/SKILL.md +10 -10
  48. package/templates/skills/skill-manager/SKILL.md +264 -0
  49. package/templates/skills/target-openfl-native/SKILL.md +52 -17
  50. package/templates/skills/target-openfl-native/references/build-and-versions.md +7 -4
  51. package/templates/skills/task-delegation/SKILL.md +76 -4
  52. package/templates/skills/web-reading/SKILL.md +33 -25
  53. package/templates/skills/workflow-authoring/SKILL.md +47 -12
  54. package/templates/subagent.yaml +29 -8
  55. package/templates/testing.yaml +64 -10
  56. package/templates/web-research.yaml +23 -13
@@ -1,9 +1,16 @@
1
1
  name: planning
2
2
  description: "Planning sub-workflow — explore, design plan, record workflow context"
3
- initial: explore_needed
3
+ initial: load_skills
4
4
  max_transitions: 30
5
5
 
6
6
  states:
7
+ load_skills:
8
+ skills:
9
+ - coding-skill-selector
10
+ - ?project-skill-selector
11
+ transitions:
12
+ continue: explore_needed
13
+
7
14
  explore_needed:
8
15
  prompt: |
9
16
  Determine if codebase exploration is needed before planning.
@@ -24,10 +31,15 @@ states:
24
31
  on_fail: design
25
32
 
26
33
  design:
34
+ # The Resume line in the plan-file template below — `transition("<session_id>", "planned")`
35
+ # — is load-bearing: hooks/workflow-start.sh regex-matches this exact positional
36
+ # form (transition\(\\"[a-f0-9]+\\") to recover the session ID on plan resume.
37
+ # Keep it EXACTLY as is; do not normalize to named arguments.
27
38
  prompt: |
28
- Load architecture skill: `Skill("architecture")`.
29
-
30
- Design the implementation plan.
39
+ Design the implementation plan. (Coding skills incl. architecture
40
+ and the project's structural-navigation skill were loaded at the
41
+ `load_skills` gate follow them; do not default to grep on
42
+ parseable source.)
31
43
 
32
44
  **Plan mode**: if system prompt says "Plan mode is active" — you're already in it,
33
45
  do NOT call EnterPlanMode. Otherwise call `EnterPlanMode`.
@@ -61,7 +73,7 @@ states:
61
73
  `Background`) unless the task is genuinely novel and context is needed.
62
74
 
63
75
  **Steps in plan mode:**
64
- 1. Explore codebase (Read/Glob/Grep or Plan/Explore subagents)
76
+ 1. Explore codebase (directly or via Plan/Explore subagents)
65
77
  2. Design approach
66
78
  3. Write plan to the plan file specified in plan mode system prompt
67
79
  4. Plan file MUST end with Workflow section (get session_id via `status`):
@@ -70,17 +82,21 @@ states:
70
82
  - **Session**: `<session_id>`
71
83
  - **Current state**: `planning @ design`
72
84
  - **After approve**: transition `planned` → returns to parent
73
- - **Execution workflow**: coding / code-review / <workflow-name>
85
+ - **Execution workflow**: coding / feature / bug_fix / testing / refactoring / <workflow-name>
74
86
  - **Resume**: `transition("<session_id>", "planned")`
75
87
  ```
76
88
  5. Call `ExitPlanMode` for user approval
77
89
 
78
90
  **After plan approved → transition `planned`.**
91
+ **If the user rejects the plan** → revise the plan file per their feedback,
92
+ then call `ExitPlanMode` again. Do NOT transition until approved.
79
93
  transitions:
80
94
  planned: done
81
95
  dynamic: create_workflow
82
96
 
83
97
  create_workflow:
98
+ # Resume line in the template below: same load-bearing positional form as in
99
+ # `design` (hook regex) — keep `transition("<session_id>", "planned")` exactly.
84
100
  prompt: |
85
101
  The task is operational/dynamic — create the execution workflow directly.
86
102
  The workflow IS the plan.
@@ -101,10 +117,21 @@ states:
101
117
  4. Write plan file with:
102
118
  - Brief context of what the workflow does
103
119
  - Reference to the created workflow name
104
- - Workflow section with `Execution workflow: <workflow-name>` (actual name, not "dynamic")
120
+ - The FULL Workflow section (get session_id via `status`):
121
+ ```
122
+ ## Workflow
123
+ - **Session**: `<session_id>`
124
+ - **Current state**: `planning @ create_workflow`
125
+ - **After approve**: transition `planned` → returns to parent
126
+ - **Execution workflow**: <workflow-name> (actual name, not "dynamic")
127
+ - **Resume**: `transition("<session_id>", "planned")`
128
+ ```
105
129
  5. Call `ExitPlanMode` for user approval
106
130
 
107
131
  **After plan approved → transition `planned`.**
132
+ **If the user rejects the plan** → revise the plan file (and the workflow
133
+ definition if needed) per their feedback, then call `ExitPlanMode` again.
134
+ Do NOT transition until approved.
108
135
  transitions:
109
136
  planned: done
110
137
 
@@ -12,6 +12,7 @@ states:
12
12
  - Any hallucinations (made-up names, paths, APIs)?
13
13
  - Unnecessary complexity added?
14
14
  - User had to correct you or wait?
15
+ - Workflow/skill friction — wrong routing, missing skills, redundant states?
15
16
 
16
17
  Choose transition:
17
18
  - `has_lesson` → found something worth recording
@@ -43,11 +44,17 @@ states:
43
44
  - `suggest_improvements` → the lesson itself IS about workflow/skill friction
44
45
  (not a side observation — the primary lesson is about process inefficiency)
45
46
  transitions:
46
- enrich_skill: action
47
- create_skill: action
47
+ enrich_skill: load_skill_manager
48
+ create_skill: load_skill_manager
48
49
  update_memory: action
49
50
  suggest_improvements: suggest_improvements
50
51
 
52
+ load_skill_manager:
53
+ skills:
54
+ - skill-manager
55
+ transitions:
56
+ continue: action
57
+
51
58
  action:
52
59
  prompt: |
53
60
  Execute the classified action:
@@ -59,7 +66,7 @@ states:
59
66
  After acting, acknowledge the lesson to the user honestly.
60
67
 
61
68
  Choose transition:
62
- - `more` → have more lessons to classify
69
+ - `more` → more lessons to classify, or remaining actions for this lesson
63
70
  - `suggest_improvements` → also noticed workflow/skill friction
64
71
  - `done` → all lessons handled
65
72
  transitions:
@@ -86,7 +93,7 @@ states:
86
93
  Only suggest changes you're confident about — no speculative ideas.
87
94
 
88
95
  Choose transition:
89
- - `more` → still have lessons to classify from evaluate
96
+ - `more` → still have lessons to classify
90
97
  - `done` → finished
91
98
  transitions:
92
99
  more: classify
@@ -1,5 +1,5 @@
1
1
  name: review-push
2
- description: "Review uncommitted changes, then commit and push to GitHub if clean"
2
+ description: "Review uncommitted changes, then build, commit, and push to the remote if clean"
3
3
  initial: review
4
4
  max_transitions: 60
5
5
 
@@ -26,11 +26,17 @@ states:
26
26
 
27
27
  fix_build:
28
28
  task: "Fix build errors"
29
+ max_visits: 5
29
30
  prompt: |
30
31
  Build failed. Fix the errors, then build again.
32
+
33
+ Budget: 5 visits to this state (engine-enforced); when nearly exhausted
34
+ and the build still fails, choose `give_up` instead of retrying.
35
+
31
36
  After fixing → `retry`.
32
37
  transitions:
33
38
  retry: build
39
+ give_up: failed
34
40
 
35
41
  commit:
36
42
  task: "Commit changes"
@@ -40,9 +46,14 @@ states:
40
46
  1. `git status` to see what's being committed
41
47
  2. `git diff --stat` for a summary
42
48
  3. `git log --oneline -5` to match commit message style
43
- 4. Stage relevant files (prefer specific files over `git add -A`)
44
- 5. Write a clear commit message following the repo's convention
45
- 6. Commit
49
+ 4. Confirm you are on the intended branch (`git branch --show-current`)
50
+ if not, stop and sort the branch out first
51
+ 5. Stage relevant files (prefer specific files over `git add -A`)
52
+ 6. Scan the staged diff for secrets (`git diff --cached` — API keys,
53
+ tokens, passwords, credentials, .env contents). Found any → unstage
54
+ the file, add it to .gitignore, and report it
55
+ 7. Write a clear commit message following the repo's convention
56
+ 8. Commit
46
57
 
47
58
  Then → `push`.
48
59
  transitions:
@@ -52,12 +63,20 @@ states:
52
63
  task: "Push to remote"
53
64
  prompt: |
54
65
  Push the commit to the remote repository.
55
- Run `git push` and verify it succeeds.
56
- Report the commit hash and remote URL.
66
+
67
+ 1. Run `git push`
68
+ 2. Verify it succeeded — push output shows the ref update and
69
+ `git status` reports the branch is up to date with the remote
70
+ 3. Report the commit hash and remote URL
71
+
72
+ Then → transition `done`.
73
+ transitions:
74
+ done: finish
75
+
76
+ finish:
57
77
  terminal: true
58
78
  outcome: complete
59
79
 
60
80
  failed:
61
- prompt: "Review or build failed."
62
81
  terminal: true
63
82
  outcome: fail
@@ -21,7 +21,7 @@ Three questions (if you can't answer all three concretely, suggest the simpler o
21
21
  **GOTCHA:** Single implementation behind interface is ceremony, not architecture
22
22
  **EXCEPTION:** Public library APIs only
23
23
 
24
- Sonnet defaults to "interfaces for testing are fine" — this skill corrects that.
24
+ Models often default to "interfaces for testing are fine" — this skill corrects that.
25
25
 
26
26
  ## Response Template
27
27
 
@@ -107,6 +107,17 @@ When moving filtering logic from a caller into a shared state manager (allocator
107
107
 
108
108
  **Rule:** A generic `isX()` that returns the same answer for semantically different states is a lossy abstraction. Before using it, check: does the original code care *why* the state is set, not just *that* it's set?
109
109
 
110
+ ## Deleting a Mechanism for ONE Rejected Job — Enumerate ALL Its Jobs First
111
+
112
+ When you remove a function/call because one of its responsibilities is rejected or obsolete, list **every** side effect it has before deleting it. A mechanism named for one concern often quietly carries a second, still-needed concern — and the name masks it, so wholesale deletion silently disables the second.
113
+
114
+ **DON'T:** See "this does the rejected X" → delete the whole call/function.
115
+ **DO:** Trace every effect (what it sets, reveals, schedules, gates). Keep the still-needed effects; remove only the rejected one — often by splitting the dual-purpose function.
116
+
117
+ **Tell:** the deleted thing was a per-frame/lifecycle hook whose name describes only one job (`dodge`, `sync`, `refresh`); the regression appears in an *adjacent* concern (visibility, enablement, layout), not the one you were targeting — and surfaces late (doc/visual pass), not in the correctness check.
118
+
119
+ **Concrete example (illustrative):** A per-frame `noteActorPosition` did TWO things via one alpha lane — faded decorative props IN (made the field *visible*) and faded them OUT near the moving actor (the *proximity dodge*). The user rejected the dodge (alpha-hiding the path). Deleting the whole call removed the dodge **and** the field's visibility → the entire decorative prop field went invisible (`alpha=0` forever). Fix: split — keep a one-time fade-IN (reveal) gated by a `fadeIn` flag, drop only the per-frame fade-OUT. The reveal and the dodge had been fused in one function; the name ("dodge") hid the reveal.
120
+
110
121
  ## Refactoring Scope: Don't Escalate Lifecycle
111
122
 
112
123
  When refactoring an inline operation into a two-phase approach (set intent + execute), match the original operation's lifecycle scope.
@@ -118,6 +129,23 @@ When refactoring an inline operation into a two-phase approach (set intent + exe
118
129
 
119
130
  **Rule:** Before adding lifecycle management to a refactored operation, check: did the original operation need it? If the original worked within existing infrastructure (setters, listeners), the refactored version should too.
120
131
 
132
+ ## Signature Change → Grep All Callsites
133
+
134
+ When changing a method's return type, parameter list, or visibility, grep ALL callsites BEFORE finalizing the change.
135
+
136
+ A signature change compiles fine in the modified file but silently breaks callers that ignore the new return type. Example: `clearData(): void` → `clearData(): ITask` — callers that fired-and-forgot still compile; the returned ITask is constructed and discarded; the async work never starts. No compile error, no exception — the cleanup pipeline is a no-op.
137
+
138
+ **Symptom**: behavior silently regresses in code paths nobody runs during initial smoke test. Caught later by code reviewers, integration tests, or production bugs.
139
+
140
+ **DON'T:** Change a signature, run the obvious test, declare done.
141
+ **DO:** Change a signature, grep `<methodName>\b` across the whole project, audit each callsite. Trust "find references" only if the IDE has full project indexing.
142
+
143
+ **Adding a member to an interface/abstract type is also a signature change.** Every implementor must gain the member — including hand-written test doubles / fakes / in-memory stubs, not just production classes.
144
+
145
+ **Tooling gotcha:** test runners that strip types via esbuild/swc (vitest, ts-jest `isolatedModules`, bun test) do NOT typecheck. A test double missing a newly-added interface method compiles fine and fails at RUNTIME as `TypeError: x.method is not a function` — often in unrelated tests that happen to reach the missing call. `tsc --noEmit` catches it; the test run alone does not.
146
+
147
+ **DO:** after adding an interface member, grep for `implements <Interface>` and for the type used as a field/param type, and update every implementor (prod + test) in the same change; run `tsc` (not just the test suite) before declaring done.
148
+
121
149
  ## New Canonical Path → Ask About Legacy Parallels
122
150
 
123
151
  When you add a new canonical input/output mechanism that does the same thing as existing legacy paths (e.g. UI buttons that replicate keyboard shortcuts, a typed API replacing an untyped one, a structured event replacing a string topic), the default urge is to keep the legacy paths "for compatibility" and build a coexistence shim. Don't.
@@ -130,3 +158,105 @@ When you add a new canonical input/output mechanism that does the same thing as
130
158
  **Symptom of violation:** you find yourself adding gating logic (coordinate checks, `if (ev.target === ...)` filters, `inflight` flags) just to make two input paths coexist when one is meant to subsume the other.
131
159
 
132
160
  **Rule:** A new canonical mechanism is an opportunity to remove the old one, not to add a referee between them. If the user didn't explicitly ask to keep both, ask before keeping both.
161
+
162
+ ## Accumulation/Leak Bugs: Fix the Lifecycle, Not a Sweep
163
+
164
+ When something grows unbounded (state files, DB rows, temp dirs, handles, cache entries), the artifact has a creation event but no deletion event tied to its owner's lifecycle.
165
+
166
+ **DON'T:** Add a periodic retention/TTL/prune sweep ("delete things older than N days"). It leaves the leak in place, adds a tunable nobody sets correctly, and needs a guard against its own edge cases (NaN window deleting everything).
167
+ **DO:** Tie the artifact's lifetime to its owner. Delete it at the terminal transition (session ends → its file is removed; process dies → its records go). The owner that creates it owns destroying it.
168
+
169
+ **Tell:** if your fix introduces a `RETENTION_DAYS`/`maxAge`/cleanup-cron, ask "why does this outlive its owner at all?" A retention sweep is a legitimate *policy* for audit data deliberately kept; it is the wrong tool for a missing teardown.
170
+
171
+ **Before designing the fix:** check sibling/related projects for an already-shipped fix of the same bug class (same author/org repos especially) and mirror its approach instead of inventing a parallel one.
172
+
173
+ ## Repeated Same Symptom After 2+ Fixes = Wrong Subsystem Target
174
+
175
+ When the user reports the same perceived symptom after two or more rounds of plausible fixes, stop patching that surface. A symptom that survives multiple reasonable fixes is almost always at the **seam between two decoupled subsystems** — a baked/precomputed path vs. a live event; a cached value vs. its source; a predicted position vs. an independently-simulated actor.
176
+
177
+ **DON'T:** Keep refining one side each iteration — curve shape, easing, constants, thresholds — to match the user's latest wording.
178
+
179
+ **DO:** By the 2nd repeat, trace BOTH subsystems end-to-end and ask: "why do these two representations exist, and must they?" The fix is to make them **coincide** — re-derive one from the other at the authoritative moment — not to tune one side.
180
+
181
+ **Tell:** Every "fix" addresses the user's latest description verbatim, yet the user keeps saying "same thing / didn't help." You find yourself adjusting geometry or timing constants repeatedly with no lasting effect.
182
+
183
+ **GOTCHA:** The seam is invisible when you look at only one subsystem. You must trace both from their shared input to their diverging output paths to see the gap.
184
+
185
+ **Concrete example:** A game ball flew a fixed pre-baked bézier to a predicted endpoint; the actual hit fired from a per-frame overlap test against a separately-simulated moving sprite. Four curve-shape fixes (overshoot, speed, bulge, straight-line) all failed because the real defect was the timing seam between the two decoupled systems. Resolution: re-derive the sprite's velocity at the true flight start so ball and sprite converge at one shared point. Cost: 5 user round-trips before the architecture was traced.
186
+
187
+ ## Regressing Feature? Find the Principled Algorithm the Codebase Already Has
188
+
189
+ When a feature keeps regressing across many patch cycles — tuning constants, reshaping curves, adjusting thresholds — stop inventing ad-hoc logic. Ask: **"what does correct behavior fundamentally require, and does this codebase already compute that for some other case?"**
190
+
191
+ **DON'T:** Keep hand-rolling case-specific geometry or heuristics to chase the latest symptom. Each variant is "more specific" than the last and never converges.
192
+
193
+ **DO:** Grep for the concept (clearance, retry, normalization, lift…). If a general implementation exists, find why the failing case is excluded from it — an early `return`, a zone guard, a capability flag. That exclusion is the smoking gun: someone already solved it generally, then deliberately opted out the hard case. Mirror or apply that proven algorithm to the excluded case, using its same constants and sampling strategy for parity. Ask the "do we already do it elsewhere?" question by the **2nd** regression, not the 6th.
194
+
195
+ **Tell:** You're writing the Nth variant of the same code path and each fix is narrower than the last. A sibling module has a richer version of the same routine that the failing path doesn't call.
196
+
197
+ **GOTCHA:** The exclusion guard often looks load-bearing (e.g., "air shots have no terrain"). Verify it with the principled algorithm before assuming it's correct — it may just be an early simplification that was never revisited.
198
+
199
+ **Concrete example (illustrative):** An air-launched projectile kept clipping terrain through ~6 patch iterations (overshoot tweaks, speed adjustments, straight-line approximations). The math package already had a `raiseArcOverTerrain` routine — raises a bézier peak until it clears terrain minus a margin — but it explicitly `return`ed early for `zone === 'air'`. Fix: mirror that exact algorithm renderer-side for the excluded zone. No amount of curve reshaping could have matched it because the principled clearance math was simply never reached.
200
+
201
+ ## Changing a Value Invalidates State Sized From Its OLD Value — Not Just Live Readers
202
+
203
+ When you change a parameter X, the instinct is to update code that **reads** X. But state that was **computed/staged earlier** from X's old value (a precomputed position, a cached offset, a buffer size, a timeout deadline) doesn't "read" anything — it silently encodes the stale quantity. Those frozen values contradict the new X, often by the full ratio of old/new.
204
+
205
+ **DON'T:** Change X, fix the obvious live consumers, ship. The stale-derived state then fires with pre-change geometry/timing intact.
206
+
207
+ **DO:** When changing X, enumerate not just "who reads X now" but "what was sized/positioned/scheduled FROM X (possibly long before X changed)." Re-derive those from the new X, or trigger their recompute at the moment X takes effect.
208
+
209
+ **Tell:** a downstream actor moves/scales ~the ratio of old:new too far/fast (e.g. ~10×); a thing that used to line up now overshoots by a suspiciously clean multiple; bug magnitude ≈ `oldValue / newValue`.
210
+
211
+ **GOTCHA:** The stale computation may happen at a different time (launch, init, precompute pass) than the code you just changed (tick, render, callback). Grep for every site that captures a derived value from X into a local variable, struct field, or closure — those are the freeze points.
212
+
213
+ **Concrete example:** A projectile's flight duration was shortened ~6×. The flight-playback code was updated. But an interceptor's ENTRY POSITION had been staged at launch as `start + speed · OLD_duration` (far away, sized for the long flight); the resync only re-solved its velocity over that stale far position → it streaked in ~10× too fast. Fix: re-STAGE the entry from the new duration (`position + velocity · newDuration`), not just re-solve velocity. The corrected duration must drive every derived value — including positions frozen before the change — from a single source.
214
+
215
+ ## "Impossible in the Limit" ≠ "Cheap Lever Fails the Actual Case"
216
+
217
+ When a global constant's effect scales with a per-case input, no single value can satisfy ALL inputs — that proof is correct. But it does NOT establish that the constant fails the specific case in front of you.
218
+
219
+ **DON'T:** Write an elegant impossibility argument for the general case and propose a per-case solver/refactor without having run the simple fix once.
220
+ **DO:** Try the cheapest knob empirically first — adjust the constant, widen the relevant tolerance, measure the real target. Reserve the heavy solution for when the simple lever is *measured* to fail, not when it's only *proven* imperfect in the limit.
221
+
222
+ **Principle:** "Can't be perfect for all" ≠ "doesn't work for this." Empirical failure on the actual target is required evidence; theoretical failure on adversarial inputs is not.
223
+
224
+ **Tell:** You're composing a sound argument for why a simple fix "structurally cannot work" and proposing architecture instead — without having run the simple fix once. That's the signal to stop, try the knob, and measure.
225
+
226
+ ## A Claimed-Negative Tradeoff Needs the Same Evidence as a Claimed-Impossible Fix
227
+
228
+ The mirror of "Impossible in the Limit ≠ Cheap Lever Fails": just as you must not call a simple fix impossible without running it, do not tell the user a change makes things WORSE without deriving the actual result. A negative framing extrapolated from a stale note, a prior decision, or an intuitive "it'll scatter / fragment / break" mental model is not evidence.
229
+
230
+ **DON'T:** Warn "this will be markedly worse" / "destroys the existing structure" from memory or intuition, then ask the user whether to proceed.
231
+ **DO:** Run the concrete analysis that confirms or refutes the claim FIRST — then present the measured result, and only flag a downside that survives the measurement.
232
+
233
+ **Tell:** You're about to present a tradeoff as a downside, and your reason is a remembered decision ("we left this alone before") or a shape you pictured ("members will scatter"), not something you computed on the actual artifact.
234
+
235
+ **Concrete example:** Asked to apply an automated member-reorder to a 12k-line god-file, the agent told the user it would be "markedly worse" — destroying intentional locality, exploding one guard block into scattered fragments — citing a stale "leave this file alone" note. The user pushed back: "why worse? I expect better." The analysis the agent should have done first: a sorted line-multiset diff (proved the reorder was a pure permutation — zero content changed) and an index-contiguity + identical-condition-dedup check (proved the guarded cluster stayed one contiguous block). The real change was mild and arguably cleaner. The unmeasured negative cost a user round-trip.
236
+
237
+ **Principle:** "I think this is worse" is a hypothesis, not a finding. Before it reaches the user as a reason to hesitate, derive it on the real input — the cost of measuring is usually one command; the cost of a wrong negative is a wasted round-trip and a user who now distrusts your tradeoff calls.
238
+
239
+ ## "How Did It Work Before?" = Revert Signal
240
+
241
+ When the user asks "how did the old way work?", "why not keep it simple / slightly different?", or "did you even try the original approach?" — that is not a request to polish the new mechanism. It is evidence the new direction is wrong.
242
+
243
+ **DON'T:** Defend the new design across multiple fix→verify cycles, trading one symptom for another, while the user keeps invoking prior behavior.
244
+ **DO:** By the 2nd such pushback, diff against the pre-change baseline. Articulate honestly what the original did and why it was adequate. Offer to revert before sinking more cycles.
245
+
246
+ **Tell:** You are on the 3rd+ iteration fixing your own new mechanism; each fix introduces a new symptom; the user's questions consistently reference what it used to do. The thrash itself is the signal — the baseline was right.
247
+
248
+ **Principle:** Repeated pushback that references prior/simpler behavior is not polish feedback — it is a direction correction. Reverting to a known-good baseline is a first-class fix, not a failure.
249
+
250
+ ## The User's Named Beat IS the Acceptance Criterion
251
+
252
+ When the requirement names a concrete observable event — a strike, a bounce, a wobble, a specific sequence — that beat is not a flourish. It is the spec. Reproducing the surrounding motion while omitting the named beat is a wrong implementation, not a simpler one.
253
+
254
+ **Observed failure:** User asks for "X hits a corner and visibly bounces, then branches." Agent repeatedly delivers a fast ease-to-stop or a smooth pass-through — dropping the bounce — and declares it done. Each simplification felt cleaner or more physically plausible. User rejected it each time: "there is no bounce, this is identical to the plain case." Required explicit re-statement of the task before the beat was built.
255
+
256
+ **Tell:** You're choosing a motion because it's easier to implement or "reads cleaner," and the specific event the user named is no longer distinctly visible in the result. Or: the user says the new thing "looks the same as the old/plain one."
257
+
258
+ **DON'T:** Trade away a named beat for implementation simplicity or "physical plausibility" concerns without surfacing the trade-off.
259
+ **DO:**
260
+ 1. Enumerate every named beat from the request.
261
+ 2. For each, define how it will be **distinctly observable** (numerically or visually) and assert it before claiming done.
262
+ 3. If you're about to drop a beat because it's hard or feels unphysical, surface that trade-off explicitly — don't silently omit it.
@@ -7,30 +7,30 @@ description: AWS Lambda .NET deployment gotchas
7
7
 
8
8
  ## VPC Networking
9
9
 
10
- - **Subnet IP exhaustion on scale-up.** Each ENI consumes an IP from subnet. ENI account limit = 350 default. New instances silently fail to start when exhausted.
11
- - **ENI creation lag: up to 90s** when function is created or VPC config changes. Invocations during this window get unpredictable cold starts.
10
+ - **ENIs are shared since the Hyperplane rework (2019)** one ENI per subnet + security-group combo, created when the function is created or its VPC config changes, NOT per concurrent execution. Scale-up does not consume a subnet IP per instance, so subnet IP exhaustion from Lambda scaling is largely a pre-2019 concern.
11
+ - **ENI provisioning takes up to ~90s** at function create / VPC-config change; the function reports `LastUpdateStatus: InProgress` until done. (The old per-invoke ENI cold-start penalty is gone.)
12
12
  - **S3/Secrets Manager calls timeout silently (not connection refused)** when Lambda is in VPC without NAT/VPC Endpoint. Easy to misdiagnose as SDK bug.
13
13
 
14
14
  ## Logging
15
15
 
16
- - **`Console.Error.WriteLine()` does NOT appear in CloudWatch.** Use `ILambdaContext.Logger.LogLine()` in handler code or `ILogger` in ASP.NET pipeline.
16
+ - **`Console.Error.WriteLine()` IS captured by CloudWatch on the managed .NET runtime** (Lambda redirects the process's stdout/stderr) — but entries lack the request-id prefix and multi-line output splits into separate events. Output genuinely disappears only in narrower cases: writes during a crashed INIT (process torn down before flush) or a custom-runtime bootstrap that doesn't forward stderr. Prefer `ILambdaContext.Logger.LogLine()` in handler code or `ILogger` in the ASP.NET pipeline.
17
17
  - **Missing `logs:CreateLogGroup` permission = logs silently lost.** Log group created lazily on first invoke. No permission = no log group = nowhere to write = no error visible anywhere.
18
18
 
19
19
  ## ASP.NET Core in Lambda
20
20
 
21
- - **`UseHttpsRedirection()` hangs Lambda.** No Kestrel HTTPS port. Middleware hangs trying to discover one (aws-lambda-dotnet #1543). Only enable in Development.
22
- - **Response > 6MB crashes INIT phase, not 413.** `APIGatewayHttpApiV2ProxyFunction` throws exception in Lambda runtime client during initialization, taking down the entire ASP.NET Core app instead of returning HTTP error.
21
+ - **`UseHttpsRedirection()` hangs Lambda.** No Kestrel HTTPS port. Middleware hangs trying to discover one (aws-lambda-dotnet #1543; discussion #1424 reports the same shape — handler completes but the response never reaches the runtime — as warm-invoke middleware timeouts on a custom runtime). Only enable in Development.
22
+ - **Response > 6MB fails the invoke no clean HTTP 413.** 6MB is the synchronous INVOKE-phase request/response payload limit (nothing to do with INIT). `APIGatewayHttpApiV2ProxyFunction` surfaces it as a runtime-client error for that invocation, so the caller sees a generic 500-style failure instead of a 413.
23
23
 
24
24
  ## S3 from Lambda
25
25
 
26
26
  - **SDK default timeout = infinite, retry = 4 with exponential backoff.** Total wait 30-60s can exceed Lambda timeout. Always set `Timeout` and `MaxErrorRetry` on `AmazonS3Config`.
27
- - **`AmazonS3Client` without explicit region defaults to `us-east-1`.** Cross-region requests are slower. No error if IAM allows it silent performance degradation.
28
- - **IMDS credential error is cached.** If Instance Metadata Service is transiently unavailable during first init, SDK caches the error. All subsequent requests fail until container restart.
27
+ - **Region fallback to `us-east-1` is a local-dev gotcha, not a Lambda one.** Inside Lambda the SDK resolves the region from the `AWS_REGION` env var the service injects. `new AmazonS3Client()` without explicit region silently targets `us-east-1` only when no env/profile region is configured — e.g. local integration tests causing slow cross-region requests with no error.
28
+ - **IMDS does not exist inside Lambda.** Credentials come from env vars (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`/`AWS_SESSION_TOKEN`) injected from the execution role. Any "IMDS credential error is cached until restart" advice applies to EC2/ECS, not Lambda.
29
29
 
30
30
  ## .NET Cold Start
31
31
 
32
- - **ReadyToRun (R2R) compiled on macOS/Windows is ignored in Lambda (Amazon Linux 2).** Must publish with `--runtime linux-x64`. R2R flag silently has no effect cross-platform.
33
- - **Container deploy: INIT phase is billed. ZIP deploy: INIT phase is free.** For .NET with heavy startup, significant cost difference.
32
+ - **ReadyToRun (R2R) output is RID-specific — publish with `--runtime linux-x64` (or `linux-arm64`).** Since .NET 6, crossgen2 supports cross-OS compilation, so building ON macOS/Windows is fine as long as the target RID matches the Lambda runtime (dotnet8 runs on Amazon Linux 2023). R2R for the wrong RID silently has no effect.
33
+ - **INIT-phase billing was standardized 2025-08: INIT is billed for ALL runtimes and packaging types.** Previously ZIP-deployed managed runtimes got the INIT phase free (container images were always billed). Heavy .NET startup now costs money either way — trim startup work or use SnapStart.
34
34
  - **EF Core `Database.Migrate()` default command timeout = 30s.** Heavy migrations fail with EF timeout, not Lambda timeout. Set `CommandTimeout` explicitly.
35
35
 
36
36
  ## Function URL
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: browser-verify
3
+ description: Browser-automation verification gotchas — stale captures, caches, headless limits
4
+ ---
5
+
6
+ ## Browser-Automation Verification Gotchas
7
+
8
+ Traps when verifying web/canvas apps through Playwright or a browser MCP.
9
+ Common theme: **the automated browser is not the user's browser** — its
10
+ pixels, caches, and performance all lie in specific, repeatable ways.
11
+
12
+ ### 1. Headless screenshot / `canvas.drawImage()` can return stale frames for a WebGL canvas
13
+
14
+ Animated content driven by per-frame GPU updates (custom shader uniforms, skeletal animations, shader-displaced vertices) may show **zero pixel diff** between headless screenshots taken 100s of ms apart, even when JS-side instrumentation confirms the state is updating each frame (RAF firing, uniform buffers advancing, ticker handlers incrementing). Sampling the canvas through `tmp.getContext('2d').drawImage(canvas, ...)` returns the same byte-for-byte image across samples — the whole canvas is affected, not just one animation.
15
+
16
+ Likely cause: with `preserveDrawingBuffer: false` (the default in most WebGL frameworks, incl. Pixi), the headless compositor reads from a cached present frame rather than the live WebGL back buffer.
17
+
18
+ - Don't rely on headless pixel-diff to verify GPU-driven animation works.
19
+ - Verify in a real browser tab (the user's running Chrome, not an automated instance).
20
+ - For automated verification, prefer JS-side state assertions: a uniform buffer value advancing across samples is sufficient proof the upload pipeline is alive; visual verification needs human eyes or a non-headless captured video.
21
+
22
+ ### 2. Headed screenshots can ALSO be stale — a tab that was never frontmost composites its last presented frame
23
+
24
+ The same failure occurs HEADED whenever the target tab/window is occluded or was created behind another tab: `page.screenshot()` returns the last frame the browser's compositor ever presented for that tab. Observed: the screenshot showed a boot preloader frozen at "96%" while JS-side numeric sampling in the SAME page proved the app was live (object positions advancing every 100 ms, game logs firing). DOM inspection confirmed the preloader element was long gone — the "96%" pixels existed only in the stale compositor frame.
25
+
26
+ A second page created with `ctx.newPage()` and driven while another tab holds focus reproduces this; a page brought to front BEFORE its WebGL content started presenting captures real frames fine.
27
+
28
+ - Wrong: `ctx.newPage()` for a background capture tab, drive it, screenshot it while another tab stays focused — canvas pixels freeze at whatever was on screen when it lost focus (or never had it).
29
+ - Right:
30
+ - Reuse the context's initial tab (`ctx.pages()[0]`) rather than stacking `newPage()`s for capture work.
31
+ - Call `page.bringToFront()` immediately after `goto`, BEFORE the canvas first presents — not right before the screenshot.
32
+ - Treat JS-side numeric sampling (positions, probe state) as ground truth — it stays correct even when pixels are stale.
33
+ - For A/B visual comparisons, run each variant in its own sequential frontmost tab, never two tabs alternating.
34
+
35
+ ### 3. Playwright MCP browser reuses a PERSISTENT cache across sessions — it can serve STALE js modules / outdated app logic
36
+
37
+ The Playwright MCP browser keeps a persistent profile + HTTP cache between conversations (`~/Library/Caches/ms-playwright-mcp/...` on macOS). A plain `browser_navigate` does NOT clear it, so when verifying app behavior against a dev server (e.g. Vite), the MCP browser can run **stale js modules** (a previous session's app-logic module) while the server already serves current code. The same seed/input then produces a DIFFERENT outcome in the MCP browser vs the user's real browser — and you'll confidently report the wrong result.
38
+
39
+ - Symptom: same dev server, same input, **different console result logs** in your browser vs the user's; your "verified" outcome contradicts what the user sees on screen.
40
+ - Red herring: a differing console source-LINE for the same log (e.g. playwright reports `logic.ts:103`, user's DevTools shows `:297`) is NOT a version mismatch — playwright reports the *transformed-module* line, DevTools the *sourcemapped source* line. Same code.
41
+ - Ground truth = the LIVE console log of the actual run in a FRESH browser, AND the user's real browser. NEVER a scanner-only prediction: a scanner's dynamic `import()` can be cached independently of the static import the live app uses, so the two disagree.
42
+ - Fix when your result conflicts with the user's: suspect YOUR cache first. `browser_close` then re-navigate relaunches a fresh browser instance — clears in-memory state and stale module instances, often enough against a dev server — but the default persistent mode reuses the same on-disk profile, so the HTTP disk cache/cookies survive. Guaranteed clean: run the MCP server with `--isolated`, point `--user-data-dir` at a throwaway dir, or delete the profile dir (`~/Library/Caches/ms-playwright-mcp/mcp-<channel>-<hash>` on macOS).
43
+ - `tsc`/`vitest` run via node against DISK code, so they stay valid for code correctness; only the BROWSER render/logic can be cache-stale. Re-verify any live finding in a freshly-cleared browser before claiming it.
44
+ - Cost when ignored: ~6 user round-trips insisting an input produced X while the user kept seeing Y — the user was right; the automated browser was stale.
45
+
46
+ ### 4. Headless WebKit/Chromium on a desktop machine is NOT GPU-bound — useless for measuring MOBILE GPU perf deltas
47
+
48
+ When optimizing a WebGL scene for mobile/Safari GPU cost (resolution cap, MSAA, blend-mode overdraw, fill-rate), driving it with Playwright headless WebKit (or Chromium) on a desktop gives a **flat ~60 fps regardless of the renderer settings** — the desktop GPU absorbs pixel counts a phone can't. Observed: identical frame-time distribution (mean ~16.7 ms, p50 17, p99 18, 0 frames >33 ms) across BOTH `resolution=2` + MSAA on AND `resolution=1.5` + MSAA off, in the same scene. A headless FPS A/B between renderer configs shows **~0 delta by construction** and tells you nothing about the on-device win.
49
+
50
+ - Don't conclude "the optimization didn't help" from a headless FPS A/B — the bottleneck isn't reproduced.
51
+ - Measure the mobile GPU win **analytically** instead: device-pixel math (`resolution` 2.0→1.5 ⇒ `(1.5/2)² = 0.5625` ⇒ ~44% fewer fragment-shader invocations/frame), plus draw-call / additive-blend (overdraw) counting.
52
+ - Verify **correctness** directly: confirm the mobile code path APPLIED — e.g. read effective resolution as `canvas.width / parseFloat(canvas.style.width)` and expect the cap (1.5), not the raw `devicePixelRatio` (3) — and assert gating/visibility logic toggles via state, not via a screenshot pixel-diff.
53
+ - Trigger a mobile context so device-detection fires: `browser.newContext({ ...devices['iPhone 13'] })` gives iPhone UA + touch + DPR 3 + isMobile — but the ENGINE stays whatever you launched (the descriptor's `defaultBrowserType: 'webkit'` is honored only by the Playwright Test runner); launch `webkit` yourself to actually test WebKit.
54
+ - Ground truth for absolute on-device FPS is a physical phone, which Playwright cannot drive.
55
+
56
+ ### Framework-specific siblings
57
+
58
+ Rendering-framework-specific verification recipes (freezing a scene's tickers for a deterministic screenshot, sampling motion on the render ticker instead of your own rAF) live in the domain skills — e.g. `domain-pixi` for Pixi.js.
@@ -7,24 +7,37 @@ description: CMake build system gotchas
7
7
 
8
8
  ## find_package Search Order
9
9
 
10
- **No mode specified = tries MODULE first, then CONFIG** (Sonnet reverses this!):
10
+ **No mode specified = tries MODULE first, then CONFIG** (commonly reversed!):
11
11
  - First looks for `FindFoo.cmake` in CMAKE_MODULE_PATH (MODULE)
12
12
  - Then falls back to `FooConfig.cmake` (CONFIG)
13
13
  - `find_package(Foo)` might find wrong file — always specify CONFIG or MODULE explicitly
14
14
 
15
15
  ```cmake
16
- set(CMAKE_PREFIX_PATH "/opt/mylib") # Finds FooConfig.cmake
17
- set(CMAKE_MODULE_PATH "/opt/mylib") # Finds FindFoo.cmake — NOT the same!
16
+ # CONFIG: CMAKE_PREFIX_PATH entries are PREFIXES — CMake searches
17
+ # <prefix>/lib/cmake/Foo/, <prefix>/share/cmake/Foo/, etc.
18
+ # for FooConfig.cmake or foo-config.cmake (not the prefix dir itself)
19
+ set(CMAKE_PREFIX_PATH "/opt/mylib")
20
+
21
+ # CONFIG: Foo_DIR points at the EXACT dir containing FooConfig.cmake
22
+ set(Foo_DIR "/opt/mylib/lib/cmake/Foo")
23
+
24
+ # MODULE: CMAKE_MODULE_PATH dirs are searched DIRECTLY for FindFoo.cmake — NOT the same!
25
+ set(CMAKE_MODULE_PATH "/opt/cmake-modules")
18
26
  ```
19
27
 
20
- ## CMP0077: Normal Variables Override Options
28
+ ## CMP0077: Normal Variables Override Options (add_subdirectory)
21
29
 
22
30
  ```cmake
23
- option(FOO_ENABLE_TESTS "..." OFF)
24
- set(FOO_ENABLE_TESTS ON) # Pre-3.13: ignored! 3.13+ with CMP0077 NEW: overrides option
25
- cmake_policy(SET CMP0077 NEW) # Let NORMAL variables (not cache) override options
31
+ # Parent project configures a vendored subproject:
32
+ set(FOO_ENABLE_TESTS OFF) # normal variable must be set BEFORE the option() executes
33
+ add_subdirectory(foo) # foo's CMakeLists.txt contains: option(FOO_ENABLE_TESTS "..." ON)
26
34
  ```
27
35
 
36
+ - CMP0077 **OLD**: `option()` discards the pre-set normal variable and creates the cache entry → the parent's `OFF` is lost.
37
+ - CMP0077 **NEW**: `option()` becomes a no-op when a normal variable of that name exists → the parent's `OFF` wins.
38
+
39
+ The policy is controlled by the **subproject**: `cmake_minimum_required(VERSION 3.13...)` there, or `cmake_policy(SET CMP0077 NEW)` before its `option()` calls. The parent can force it for all subprojects with `set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)` before `add_subdirectory`.
40
+
28
41
  Key: this is about **normal** variables overriding `option()` cache entries — not cache-to-cache override.
29
42
 
30
43
  ## Ubuntu apt catch2 = v2, NOT v3
@@ -54,5 +67,8 @@ endif()
54
67
  FetchContent_Declare(Foo GIT_REPOSITORY https://github.com/org/foo.git GIT_TAG v1.0)
55
68
 
56
69
  # GOOD (no git needed):
57
- FetchContent_Declare(Foo URL https://github.com/org/foo/archive/refs/tags/v1.0.tar.gz)
70
+ FetchContent_Declare(Foo
71
+ URL https://github.com/org/foo/archive/refs/tags/v1.0.tar.gz
72
+ URL_HASH SHA256=<tarball-sha256> # pin integrity — without it the download is unverified
73
+ )
58
74
  ```