From 33e18c72e2007337051eccf9e5f7d28b9cd74bd4 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 11 Jul 2026 10:12:07 +0200 Subject: [PATCH 1/5] :paperclip: Add minor improvements to opencode setup --- .opencode/agents/commiter.md | 83 ++-- .opencode/commands/implement-plan.md | 43 ++ .opencode/commands/review.md | 21 + .../skills/code-review-and-quality/SKILL.md | 397 +++++++++++++++ .../skills/security-and-hardening/SKILL.md | 457 ++++++++++++++++++ 5 files changed, 948 insertions(+), 53 deletions(-) create mode 100644 .opencode/commands/implement-plan.md create mode 100644 .opencode/commands/review.md create mode 100644 .opencode/skills/code-review-and-quality/SKILL.md create mode 100644 .opencode/skills/security-and-hardening/SKILL.md diff --git a/.opencode/agents/commiter.md b/.opencode/agents/commiter.md index 395fe6416c..66b56d1041 100644 --- a/.opencode/agents/commiter.md +++ b/.opencode/agents/commiter.md @@ -79,67 +79,44 @@ permission: ## Role You are the Penpot commit assistant. You produce git commits that follow the -repository's commit conventions exactly: an emoji-prefixed imperative -subject, a body that explains the why, and the required trailers. You do -not implement features, review code, or push branches — you commit. +repository's commit conventions. You do not implement features, review code, or +push branches — you commit. ## Required Reading -Before drafting any commit, read `.serena/memories/workflow/creating-commits.md` -end-to-end. It is the canonical source for the emoji menu, subject/body -limits, and trailer format. The summary in this file does not replace it. +Before drafting any commit, **read `.serena/memories/workflow/creating-commits.md` +end-to-end**. It is the authoritative source for the commit message format, the +emoji menu, subject/body limits, and the `AI-assisted-by` trailer. Follow it +exactly — do not improvise the format and do not restate its contents here. ## Pre-commit Workflow 1. Run `git status` to inspect the working tree. If there are unstaged or - untracked changes that are unrelated to the user's request, STOP and ask - the user how to handle them. Do not silently include unrelated work in - the commit. -2. Run `git diff --staged` (or `git diff` for unstaged changes) and review - the content. If you see secrets (API keys, tokens, passwords, private - keys, `.env` values), debug prints, or anything that does not match the - user's stated intent, STOP and tell the user before committing. -3. Pick the commit emoji from the menu in - `mem:workflow/creating-commits`. If none of the listed emojis fit, use - `:paperclip:` (other) and explain in the body why. -4. Draft the commit message (see format below), then run - `git commit -s -m "" -m ""` (or pass the message via - `git commit -s -F -` if the body has unusual characters). - -## Commit Message Format - -``` -:emoji: Subject line (imperative, capitalized, no period, <=70 chars) - -Body explaining what changed and why. Wrap at 80 chars. Use manual -line breaks; do not rely on the terminal to wrap. - -Co-authored-by: -``` - -- Subject: imperative mood, capitalized, no trailing period, max 70 chars. -- Body: wraps at 80 chars. Explain the *why*, not just the *what* — what - was wrong before, what this change does about it, and any non-obvious - trade-offs. -- `Co-authored-by` trailer is mandatory. Replace `` with your - own model identifier (e.g. `claude-sonnet-4-6`). -- `Signed-off-by` is added automatically by `git commit -s`, using the - local `git config user.name` / `user.email`. + untracked changes unrelated to the user's request, STOP and ask how to + handle them. Do not silently include unrelated work in the commit. +2. Run `git diff --staged` (or `git diff` for unstaged changes) and review the + content. If you see secrets (API keys, tokens, passwords, private keys, + `.env` values), debug prints, or anything that does not match the user's + stated intent, STOP and tell the user before committing. +3. Following the format in the doc, draft the message and run + `git commit -m "" -m ""` (or `git commit -F -` if the body has + unusual characters). The `AI-assisted-by` trailer value is provided by the + calling agent — use it verbatim. ## Constraints - Do not push. Pushing is a separate workflow handled by the user (the - agent's permission set also denies `git push*`). -- Do not run `git reset*`, `git checkout*`, `git restore*`, `git clean*`, - or `rm*` — the permission set denies these outright. If staged work - needs to be discarded, ask the user to do it. -- Do not pass `--author`. Author identity comes from the local git - config. Never guess or hallucinate a name or email. -- Do not amend a commit you did not create in this session, unless the - user explicitly asks. `git commit --amend` rewrites history and is - irreversible once pushed. -- Do not bypass pre-commit hooks (`--no-verify`) unless the user - explicitly asks, and call out the deviation in your response. -- If the user asks for something that conflicts with these rules, follow - the user's request and explain the deviation in your response. Do not - silently override the format. + permission set also denies `git push*`). +- Do not run `git reset*`, `git checkout*`, `git restore*`, `git clean*`, or + `rm*` — the permission set denies these outright. If staged work needs to be + discarded, ask the user to do it. +- Do not pass `--author`. Author identity comes from the local git config. + Never guess or hallucinate a name or email. +- Do not amend a commit you did not create in this session, unless the user + explicitly asks. `git commit --amend` rewrites history and is irreversible + once pushed. +- Do not bypass pre-commit hooks (`--no-verify`) unless the user explicitly + asks, and call out the deviation in your response. +- If the user asks for something that conflicts with these rules, follow the + user's request and explain the deviation in your response. Do not silently + override the format. diff --git a/.opencode/commands/implement-plan.md b/.opencode/commands/implement-plan.md new file mode 100644 index 0000000000..f6cb42f6b8 --- /dev/null +++ b/.opencode/commands/implement-plan.md @@ -0,0 +1,43 @@ +--- +description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the commiter subagent +agent: build +--- + +# Implement Plan + +This command is run once a plan is ready (for example, from plan mode). Execute +the plan already prepared in the current session context — it does not take +extra arguments. Follow these steps in order. + +## 1. Create the issue + +Use the **`create-issue`** skill, following the *Creating Issues from Draft Body* +flow in `mem:workflow/creating-issues`. Derive the issue title and body from the +plan. Capture the new issue's number — call it **NNNN** (needed for the branch +name and the commit reference). + +## 2. Create the branch + +Create and switch to a branch named after the issue: + +``` +git checkout -b issue-NNNN +``` + +(Replace NNNN with the issue number from step 1.) + +## 3. Execute the plan + +Implement the prepared plan from the session context. Work methodically, keeping +changes focused on what the issue requires. Do not commit — the commit happens in +step 4. + +## 4. Commit with the commiter subagent + +After the implementation is complete, delegate the commit to the **`commiter`** +subagent. Give it a brief summary of what was implemented and why, the issue +reference (`issue-NNNN`), and the model name you are running as so it sets the +`AI-assisted-by` trailer correctly. The subagent owns the commit format and +conventions. + +Do not push. Pushing is handled separately by the user. diff --git a/.opencode/commands/review.md b/.opencode/commands/review.md new file mode 100644 index 0000000000..7736620e89 --- /dev/null +++ b/.opencode/commands/review.md @@ -0,0 +1,21 @@ +--- +description: Review a commit (defaults to the last commit) with the code-review-and-quality skill across all five axes +agent: plan +subtask: true +--- + +You are performing a code review of a git commit. You MUST conduct it using the **`code-review-and-quality`** skill (the five-axis review: correctness, readability, architecture, security, performance). + +The user may specify a commit or revision range as an argument ($ARGUMENTS). If no argument is given, default to reviewing the **last commit** (`HEAD`, i.e. the changes introduced by `HEAD` vs its parent). + +Workflow: + +1. Determine the target to review: + - If the user provided a revision/range in $ARGUMENTS, use it. + - Otherwise, default to the last commit: review `HEAD` (the diff of `HEAD` against `HEAD~1`). +2. Inspect the change with `git show ` / `git diff ~1 ` and `git log -1 --stat ` to understand the intent and the files touched. +3. Invoke the **`code-review-and-quality`** skill and review the commit across all five axes. Categorize every finding as Critical / Required / Optional / Nit / FYI, and lead with correctness and security. +4. For each finding, state the axis it belongs to, the severity, and a concrete suggested fix (propose the structural remedy, not just the problem). +5. Conclude with a clear verdict: **Approve** (ready to merge) or **Request changes** (issues that must be addressed), and summarize the highest-leverage items. + +Do not modify any code and do not create a commit — this command only reviews. diff --git a/.opencode/skills/code-review-and-quality/SKILL.md b/.opencode/skills/code-review-and-quality/SKILL.md new file mode 100644 index 0000000000..21b0b71771 --- /dev/null +++ b/.opencode/skills/code-review-and-quality/SKILL.md @@ -0,0 +1,397 @@ +--- +name: code-review-and-quality +description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch. +--- + +# Code Review and Quality + +## Overview + +Multi-dimensional code review with quality gates. Every change gets reviewed before merge — no exceptions. Review covers five axes: correctness, readability, architecture, security, and performance. + +**The approval standard:** Approve a change when it definitely improves overall code health, even if it isn't perfect. Perfect code doesn't exist — the goal is continuous improvement. Don't block a change because it isn't exactly how you would have written it. If it improves the codebase and follows the project's conventions, approve it. + +## When to Use + +- Before merging any PR or change +- After completing a feature implementation +- When another agent or model produced code you need to evaluate +- When refactoring existing code +- After any bug fix (review both the fix and the regression test) + +## The Five-Axis Review + +Every review evaluates code across these dimensions: + +### 1. Correctness + +Does the code do what it claims to do? + +- Does it match the spec or task requirements? +- Are edge cases handled (null, empty, boundary values)? +- Are error paths handled (not just the happy path)? +- Does it pass all tests? Are the tests actually testing the right things? +- Are there off-by-one errors, race conditions, or state inconsistencies? + +### 2. Readability & Simplicity + +Can another engineer (or agent) understand this code without the author explaining it? + +- Are names descriptive and consistent with project conventions? (No `temp`, `data`, `result` without context) +- Is the control flow straightforward (avoid nested ternaries, deep callbacks)? +- Is the code organized logically (related code grouped, clear module boundaries)? +- Are there any "clever" tricks that should be simplified? +- **Could this be done in fewer lines?** (1000 lines where 100 suffice is a failure) +- **Are abstractions earning their complexity?** (Don't generalize until the third use case) +- Would comments help clarify non-obvious intent? (But don't comment obvious code.) +- Are there dead code artifacts: no-op variables (`_unused`), backwards-compat shims, or `// removed` comments? +- **Is a new conditional bolted onto an unrelated flow?** That's a design smell, not a nit — push the logic into its own helper, state, or policy instead of tangling an existing path. +- **Do repeated conditionals on the same shape appear?** They signal a missing model or dispatcher. A "temporary" branch is usually permanent debt. + +### 3. Architecture + +Does the change fit the system's design? + +- Does it follow existing patterns or introduce a new one? If new, is it justified? +- Does it maintain clean module boundaries? +- Is there code duplication that should be shared? +- Are dependencies flowing in the right direction (no circular dependencies)? +- Is the abstraction level appropriate (not over-engineered, not too coupled)? +- **Does this refactor reduce complexity or just relocate it?** Count the concepts a reader must hold to follow the change. If a "cleaner" version leaves that count unchanged, it isn't cleaner — prefer the restructuring that makes whole branches, modes, or layers disappear over one that re-centralizes the same logic. Prefer deleting an abstraction to polishing it. +- **Is feature-specific logic leaking into a shared or general-purpose module?** Keep logic in its owning layer, reuse the existing canonical helper instead of a near-duplicate, and don't normalize architectural drift. +- **Are type boundaries explicit?** Question gratuitous `any`/`unknown`/optional/casts and silent fallbacks that paper over an unclear invariant — making the boundary explicit often makes the surrounding control flow simpler. + +### 4. Security + +For detailed security guidance, see `security-and-hardening`. Does the change introduce vulnerabilities? + +- Is user input validated and sanitized? +- Are secrets kept out of code, logs, and version control? +- Is authentication/authorization checked where needed? +- Are SQL queries parameterized (no string concatenation)? +- Are outputs encoded to prevent XSS? +- Are dependencies from trusted sources with no known vulnerabilities? +- Is data from external sources (APIs, logs, user content, config files) treated as untrusted? +- Are external data flows validated at system boundaries before use in logic or rendering? + +### 5. Performance + +Does the change introduce performance problems? + +- Any N+1 query patterns? +- Any unbounded loops or unconstrained data fetching? +- Any synchronous operations that should be async? +- Any unnecessary re-renders in UI components? +- Any missing pagination on list endpoints? +- Any large objects created in hot paths? + +## Structural Remedies + +When you flag a structural problem, propose the move — not just the problem. A review that only says "this is complex" leaves the author guessing. Reach for a named restructuring: + +- **Replace a chain of conditionals** with a typed model or an explicit dispatcher. +- **Collapse duplicate branches** into a single clearer flow. +- **Separate orchestration from business logic** so each reads on its own. +- **Move feature-specific logic** out of a shared module into the package that owns the concept. +- **Reuse the canonical helper** instead of a bespoke near-duplicate. +- **Make a type boundary explicit** so downstream branching disappears. +- **Delete a pass-through wrapper** that adds indirection without clarifying the API. +- **Extract a helper, or split a large file** into focused modules. + +Prefer the remedy that removes moving pieces over one that spreads the same complexity around. + +## Change Sizing + +Small, focused changes are easier to review, faster to merge, and safer to deploy. Target these sizes: + +``` +~100 lines changed → Good. Reviewable in one sitting. +~300 lines changed → Acceptable if it's a single logical change. +~1000 lines changed → Too large. Split it. +``` + +**Watch file size, not just diff size.** A small diff can still push a file past a healthy boundary — around 1000 *total* lines in a single file (distinct from the ~1000 *changed*-lines threshold above) is a common inspection signal, not a hard cap. When a change materially grows an already-large file, ask whether to extract helpers, subcomponents, or modules *first*, before piling more on. Decompose, then add. + +**What counts as "one change":** A single self-contained modification that addresses one thing, includes related tests, and keeps the system functional after submission. One part of a feature — not the whole feature. + +**Splitting strategies when a change is too large:** + +| Strategy | How | When | +|----------|-----|------| +| **Stack** | Submit a small change, start the next one based on it | Sequential dependencies | +| **By file group** | Separate changes for groups needing different reviewers | Cross-cutting concerns | +| **Horizontal** | Create shared code/stubs first, then consumers | Layered architecture | +| **Vertical** | Break into smaller full-stack slices of the feature | Feature work | + +**When large changes are acceptable:** Complete file deletions and automated refactoring where the reviewer only needs to verify intent, not every line. + +**Separate refactoring from feature work.** A change that refactors existing code and adds new behavior is two changes — submit them separately. Small cleanups (variable renaming) can be included at reviewer discretion. + +## Change Descriptions + +Every change needs a description that stands alone in version control history. + +**First line:** Short, imperative, standalone. "Delete the FizzBuzz RPC" not "Deleting the FizzBuzz RPC." Must be informative enough that someone searching history can understand the change without reading the diff. + +**Body:** What is changing and why. Include context, decisions, and reasoning not visible in the code itself. Link to bug numbers, benchmark results, or design docs where relevant. Acknowledge approach shortcomings when they exist. + +**Anti-patterns:** "Fix bug," "Fix build," "Add patch," "Moving code from A to B," "Phase 1," "Add convenience functions." + +## Review Process + +### Step 1: Understand the Context + +Before looking at code, understand the intent: + +``` +- What is this change trying to accomplish? +- What spec or task does it implement? +- What is the expected behavior change? +``` + +### Step 2: Review the Tests First + +Tests reveal intent and coverage: + +``` +- Do tests exist for the change? +- Do they test behavior (not implementation details)? +- Are edge cases covered? +- Do tests have descriptive names? +- Would the tests catch a regression if the code changed? +``` + +### Step 3: Review the Implementation + +Walk through the code with the five axes in mind: + +``` +For each file changed: +1. Correctness: Does this code do what the test says it should? +2. Readability: Can I understand this without help? +3. Architecture: Does this fit the system? +4. Security: Any vulnerabilities? +5. Performance: Any bottlenecks? +``` + +### Step 4: Categorize Findings + +Label every comment with its severity so the author knows what's required vs optional: + +| Prefix | Meaning | Author Action | +|--------|---------|---------------| +| *(no prefix)* | Required change | Must address before merge | +| **Critical:** | Blocks merge | Security vulnerability, data loss, broken functionality | +| **Nit:** | Minor, optional | Author may ignore — formatting, style preferences | +| **Optional:** / **Consider:** | Suggestion | Worth considering but not required | +| **FYI** | Informational only | No action needed — context for future reference | + +This prevents authors from treating all feedback as mandatory and wasting time on optional suggestions. + +**Lead with what matters.** Order findings by leverage: correctness and security first, then structural regressions and missed simplifications, then everything else. Don't bury a real issue under cosmetic nits — a few high-conviction comments beat a long list. If you have one structural problem and ten nits, the structural problem *is* the review. + +### Step 5: Verify the Verification + +Check the author's verification story: + +``` +- What tests were run? +- Did the build pass? +- Was the change tested manually? +- Are there screenshots for UI changes? +- Is there a before/after comparison? +``` + +## Multi-Model Review Pattern + +Use different models for different review perspectives: + +``` +Model A writes the code + │ + ▼ +Model B reviews for correctness and architecture + │ + ▼ +Model A addresses the feedback + │ + ▼ +Human makes the final call +``` + +This catches issues that a single model might miss — different models have different blind spots. + +**Example prompt for a review agent:** +``` +Review this code change for correctness, security, and adherence to +our project conventions. The spec says [X]. The change should [Y]. +Flag any issues as Critical, Required, Optional, or Nit. +``` + +## Dead Code Hygiene + +After any refactoring or implementation change, check for orphaned code: + +1. Identify code that is now unreachable or unused +2. List it explicitly +3. **Ask before deleting:** "Should I remove these now-unused elements: [list]?" + +Don't leave dead code lying around — it confuses future readers and agents. But don't silently delete things you're not sure about. When in doubt, ask. + +``` +DEAD CODE IDENTIFIED: +- formatLegacyDate() in src/utils/date.ts — replaced by formatDate() +- OldTaskCard component in src/components/ — replaced by TaskCard +- LEGACY_API_URL constant in src/config.ts — no remaining references +→ Safe to remove these? +``` + +## Review Speed + +Slow reviews block entire teams. The cost of context-switching to review is less than the waiting cost imposed on others. + +- **Respond within one business day** — this is the maximum, not the target +- **Ideal cadence:** Respond shortly after a review request arrives, unless deep in focused coding. A typical change should complete multiple review rounds in a single day +- **Prioritize fast individual responses** over quick final approval. Quick feedback reduces frustration even if multiple rounds are needed +- **Large changes:** Ask the author to split them rather than reviewing one massive changeset + +## Handling Disagreements + +When resolving review disputes, apply this hierarchy: + +1. **Technical facts and data** override opinions and preferences +2. **Style guides** are the absolute authority on style matters +3. **Software design** must be evaluated on engineering principles, not personal preference +4. **Codebase consistency** is acceptable if it doesn't degrade overall health + +**Don't accept "I'll clean it up later."** Experience shows deferred cleanup rarely happens. Require cleanup before submission unless it's a genuine emergency. If surrounding issues can't be addressed in this change, require filing a bug with self-assignment. + +## Honesty in Review + +When reviewing code — whether written by you, another agent, or a human: + +- **Don't rubber-stamp.** "LGTM" without evidence of review helps no one. +- **Don't soften real issues.** "This might be a minor concern" when it's a bug that will hit production is dishonest. +- **Quantify problems when possible.** "This N+1 query will add ~50ms per item in the list" is better than "this could be slow." +- **Push back on approaches with clear problems.** Sycophancy is a failure mode in reviews. If the implementation has issues, say so directly and propose alternatives. +- **Accept override gracefully.** If the author has full context and disagrees, defer to their judgment. Comment on code, not people — reframe personal critiques to focus on the code itself. + +## Dependency Discipline + +Part of code review is dependency review: + +**Before adding any dependency:** + +1. Does the existing stack solve this? (Often it does.) +2. How large is the dependency? (Check bundle impact.) +3. Is it actively maintained? (Check last commit, open issues.) +4. Does it have known vulnerabilities? (`npm audit`) +5. What's the license? (Must be compatible with the project.) + +**Rule:** Prefer standard library and existing utilities over new dependencies. Every dependency is a liability. + +**Upgrading an existing dependency** is a code change like any other, and the riskiest upgrades are the ones merged in bulk with a message like "bump deps." Review them with the same discipline: + +1. **Read the changelog, not just the version number.** Semver is a promise the maintainer may not have kept — a "patch" can carry a behavioral change. For a major bump, read the migration notes and find what breaks. +2. **One dependency per change.** Upgrade and merge them individually (or in small related groups). When a bulk bump breaks the build, you've lost which package did it; a single-package change makes the cause obvious and the revert clean. +3. **Let the tests decide.** The upgrade is verified by a green suite before *and* after, not by "it installed." If coverage around the dependency's behavior is thin, that gap is the real finding — add a test first. +4. **Mind the transitive graph.** Most installed packages are ones nobody chose directly. Review the lockfile diff, not just `package.json`; a single direct bump can pull in dozens of indirect changes. +5. **Keep the lockfile honest.** Commit it, review its diff, and never hand-edit it. The lockfile is the thing that actually pins what ships. + +For triaging `npm audit` findings and supply-chain risk (typosquatting, compromised maintainers), follow the `security-and-hardening` skill — this section covers the upgrade *workflow*, that one covers the security verdict. + +## The Review Checklist + +```markdown +## Review: [PR/Change title] + +### Context +- [ ] I understand what this change does and why + +### Correctness +- [ ] Change matches spec/task requirements +- [ ] Edge cases handled +- [ ] Error paths handled +- [ ] Tests cover the change adequately + +### Readability +- [ ] Names are clear and consistent +- [ ] Logic is straightforward +- [ ] No unnecessary complexity + +### Architecture +- [ ] Follows existing patterns +- [ ] No unnecessary coupling or dependencies +- [ ] Appropriate abstraction level +- [ ] Refactors reduce complexity rather than relocate it +- [ ] No feature logic in shared modules; file stays within a healthy size + +### Security +- [ ] No secrets in code +- [ ] Input validated at boundaries +- [ ] No injection vulnerabilities +- [ ] Auth checks in place +- [ ] External data sources treated as untrusted + +### Performance +- [ ] No N+1 patterns +- [ ] No unbounded operations +- [ ] Pagination on list endpoints + +### Verification +- [ ] Tests pass +- [ ] Build succeeds +- [ ] Manual verification done (if applicable) + +### Verdict +- [ ] **Approve** — Ready to merge +- [ ] **Request changes** — Issues must be addressed +``` + +## See Also + +- For detailed security review guidance, see `security-and-hardening` + +## Common Rationalizations + +| Rationalization | Reality | +|---|---| +| "It works, that's good enough" | Working code that's unreadable, insecure, or architecturally wrong creates debt that compounds. | +| "I wrote it, so I know it's correct" | Authors are blind to their own assumptions. Every change benefits from another set of eyes. | +| "We'll clean it up later" | Later never comes. The review is the quality gate — use it. Require cleanup before merge, not after. | +| "AI-generated code is probably fine" | AI code needs more scrutiny, not less. It's confident and plausible, even when wrong. | +| "The tests pass, so it's good" | Tests are necessary but not sufficient. They don't catch architecture problems, security issues, or readability concerns. | +| "The refactor makes it cleaner" | Relocating complexity isn't reducing it. If the reader still holds the same number of concepts, the structure didn't improve — look for the version where branches disappear. | +| "It's only a small addition to this file" | Small diffs still push files past a healthy size and bolt branches onto unrelated flows. Judge the resulting structure, not the diff size. | +| "It's just a version bump" | A bump is a behavior change you didn't write. Read the changelog; semver doesn't guarantee no breakage. | +| "I'll upgrade everything in one PR to save time" | A bulk bump that breaks the build hides which package did it. One dependency per change keeps the cause and the revert clean. | + +## Red Flags + +- PRs merged without any review +- Review that only checks if tests pass (ignoring other axes) +- "LGTM" without evidence of actual review +- Security-sensitive changes without security-focused review +- Large PRs that are "too big to review properly" (split them) +- No regression tests with bug fix PRs +- Review comments without severity labels — makes it unclear what's required vs optional +- Accepting "I'll fix it later" — it never happens +- A refactor that moves code around without reducing the number of concepts a reader must hold +- A change that grows an already-large file instead of decomposing it +- New conditionals scattered into unrelated code paths (a missing abstraction) +- A bespoke helper that duplicates an existing canonical one, or feature logic placed in a shared module +- A bulk "bump dependencies" PR with no changelog review and no per-package isolation +- A lockfile change that's hand-edited, uncommitted, or merged without reviewing its diff + +## Verification + +After review is complete: + +- [ ] All Critical issues are resolved +- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification +- [ ] Tests pass +- [ ] Build succeeds +- [ ] The verification story is documented (what changed, how it was verified) +- [ ] Dependency upgrades were reviewed against their changelog, isolated per package, and verified by a green suite with the lockfile diff reviewed + +**Presumptive blockers:** surface and propose the simpler design for each of these; escalate to Required only when the change actively makes structure worse: a refactor that relocates complexity instead of reducing it; a change that pushes a file past the size boundary with no decomposition; feature logic added to a shared module; a near-duplicate of an existing canonical helper; a silent fallback that hides an unclear invariant. diff --git a/.opencode/skills/security-and-hardening/SKILL.md b/.opencode/skills/security-and-hardening/SKILL.md new file mode 100644 index 0000000000..a0a80bd617 --- /dev/null +++ b/.opencode/skills/security-and-hardening/SKILL.md @@ -0,0 +1,457 @@ +--- +name: security-and-hardening +description: Hardens code against vulnerabilities. Use when handling user input, authentication, data storage, or external integrations. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services. +--- + +# Security and Hardening + +## Overview + +Security-first development practices for web applications. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security isn't a phase — it's a constraint on every line of code that touches user data, authentication, or external systems. + +## When to Use + +- Building anything that accepts user input +- Implementing authentication or authorization +- Storing or transmitting sensitive data +- Integrating with external APIs or services +- Adding file uploads, webhooks, or callbacks +- Handling payment or PII data + +## Process: Threat Model First + +Controls bolted on without a threat model are guesses. Before hardening, spend five minutes thinking like an attacker: + +1. **Map the trust boundaries.** Where does untrusted data cross into your system? HTTP requests, form fields, file uploads, webhooks, third-party APIs, message queues, and **LLM output**. Every boundary is attack surface. +2. **Name the assets.** What's worth stealing or breaking? Credentials, PII, payment data, admin actions, money movement. +3. **Run STRIDE over each boundary** — a quick lens, not a ceremony: + +| Threat | Ask | Typical mitigation | +|---|---|---| +| **S**poofing | Can someone impersonate a user/service? | Authentication, signature verification | +| **T**ampering | Can data be altered in transit or at rest? | Integrity checks, parameterized queries, HTTPS | +| **R**epudiation | Can an action be denied later? | Audit logging of security events | +| **I**nformation disclosure | Can data leak? | Encryption, field allowlists, generic errors | +| **D**enial of service | Can it be overwhelmed? | Rate limiting, input size caps, timeouts | +| **E**levation of privilege | Can a user gain rights they shouldn't? | Authorization checks, least privilege | + +4. **Write abuse cases next to use cases.** For each feature, ask "how would I misuse this?" — then make that your first test. + +If you can't name the trust boundaries for a feature, you're not ready to secure it. This is OWASP **A04: Insecure Design** — most breaches begin in design, not code. + +## The Three-Tier Boundary System + +### Always Do (No Exceptions) + +- **Validate all external input** at the system boundary (API routes, form handlers) +- **Parameterize all database queries** — never concatenate user input into SQL +- **Encode output** to prevent XSS (use framework auto-escaping, don't bypass it) +- **Use HTTPS** for all external communication +- **Hash passwords** with bcrypt/scrypt/argon2 (never store plaintext) +- **Set security headers** (CSP, HSTS, X-Frame-Options, X-Content-Type-Options) +- **Use httpOnly, secure, sameSite cookies** for sessions +- **Run `npm audit`** (or equivalent) before every release + +### Ask First (Requires Human Approval) + +- Adding new authentication flows or changing auth logic +- Storing new categories of sensitive data (PII, payment info) +- Adding new external service integrations +- Changing CORS configuration +- Adding file upload handlers +- Modifying rate limiting or throttling +- Granting elevated permissions or roles + +### Never Do + +- **Never commit secrets** to version control (API keys, passwords, tokens) +- **Never log sensitive data** (passwords, tokens, full credit card numbers) +- **Never trust client-side validation** as a security boundary +- **Never disable security headers** for convenience +- **Never use `eval()` or `innerHTML`** with user-provided data +- **Never store sessions in client-accessible storage** (localStorage for auth tokens) +- **Never expose stack traces** or internal error details to users + +## OWASP Top 10 Prevention Patterns + +These are prevention patterns, not a ranking. For the 2021 ordering, see the quick-reference table in `references/security-checklist.md`. + +### Injection (SQL, NoSQL, OS Command) + +```typescript +// BAD: SQL injection via string concatenation +const query = `SELECT * FROM users WHERE id = '${userId}'`; + +// GOOD: Parameterized query +const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]); + +// GOOD: ORM with parameterized input +const user = await prisma.user.findUnique({ where: { id: userId } }); +``` + +### Broken Authentication + +```typescript +// Password hashing +import { hash, compare } from 'bcrypt'; + +const SALT_ROUNDS = 12; +const hashedPassword = await hash(plaintext, SALT_ROUNDS); +const isValid = await compare(plaintext, hashedPassword); + +// Session management +app.use(session({ + secret: process.env.SESSION_SECRET, // From environment, not code + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, // Not accessible via JavaScript + secure: true, // HTTPS only + sameSite: 'lax', // CSRF protection + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }, +})); +``` + +### Cross-Site Scripting (XSS) + +```typescript +// BAD: Rendering user input as HTML +element.innerHTML = userInput; + +// GOOD: Use framework auto-escaping (React does this by default) +return
{userInput}
; + +// If you MUST render HTML, sanitize first +import DOMPurify from 'dompurify'; +const clean = DOMPurify.sanitize(userInput); +``` + +### Broken Access Control + +```typescript +// Always check authorization, not just authentication +app.patch('/api/tasks/:id', authenticate, async (req, res) => { + const task = await taskService.findById(req.params.id); + + // Check that the authenticated user owns this resource + if (task.ownerId !== req.user.id) { + return res.status(403).json({ + error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' } + }); + } + + // Proceed with update + const updated = await taskService.update(req.params.id, req.body); + return res.json(updated); +}); +``` + +### Security Misconfiguration + +```typescript +// Security headers (use helmet for Express) +import helmet from 'helmet'; +app.use(helmet()); + +// Content Security Policy +app.use(helmet.contentSecurityPolicy({ + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], // Tighten if possible + imgSrc: ["'self'", 'data:', 'https:'], + connectSrc: ["'self'"], + }, +})); + +// CORS — restrict to known origins +app.use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000', + credentials: true, +})); +``` + +### Sensitive Data Exposure + +```typescript +// Never return sensitive fields in API responses +function sanitizeUser(user: UserRecord): PublicUser { + const { passwordHash, resetToken, ...publicFields } = user; + return publicFields; +} + +// Use environment variables for secrets +const API_KEY = process.env.STRIPE_API_KEY; +if (!API_KEY) throw new Error('STRIPE_API_KEY not configured'); +``` + +### Server-Side Request Forgery (SSRF) + +Any time the server fetches a URL the user influenced — webhooks, "import from URL", image proxies, link previews — an attacker can aim it at internal services (cloud metadata, `localhost`, private IPs). + +```typescript +// BAD: fetch whatever the user gives you +await fetch(req.body.webhookUrl); + +// GOOD: allowlist scheme + host, reject if ANY resolved IP is private, forbid redirects +import { lookup } from 'node:dns/promises'; +import ipaddr from 'ipaddr.js'; + +const ALLOWED_HOSTS = new Set(['hooks.example.com']); + +async function assertSafeUrl(raw: string): Promise { + const url = new URL(raw); + if (url.protocol !== 'https:') throw new Error('https only'); + if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('host not allowed'); + // Resolve ALL records; a single private/reserved address fails the check. + const addrs = await lookup(url.hostname, { all: true }); + if (addrs.some((a) => ipaddr.parse(a.address).range() !== 'unicast')) { + throw new Error('private/reserved IP'); + } + return url; +} + +await fetch(await assertSafeUrl(req.body.webhookUrl), { redirect: 'error' }); +``` + +The `range() !== 'unicast'` check covers loopback, link-local `169.254.169.254` (cloud metadata, the #1 SSRF target), private, and unique-local ranges across IPv4 and IPv6. + +**Caveat — this still has a TOCTOU gap.** `fetch` resolves DNS again after the check, so an attacker using a short-TTL record can rebind to an internal IP between validation and connection. For high-risk surfaces, resolve once and connect to the pinned IP, or put a filtering agent in front (`request-filtering-agent` / `ssrf-req-filter`). + +## Input Validation Patterns + +### Schema Validation at Boundaries + +```typescript +import { z } from 'zod'; + +const CreateTaskSchema = z.object({ + title: z.string().min(1).max(200).trim(), + description: z.string().max(2000).optional(), + priority: z.enum(['low', 'medium', 'high']).default('medium'), + dueDate: z.string().datetime().optional(), +}); + +// Validate at the route handler +app.post('/api/tasks', async (req, res) => { + const result = CreateTaskSchema.safeParse(req.body); + if (!result.success) { + return res.status(422).json({ + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid input', + details: result.error.flatten(), + }, + }); + } + // result.data is now typed and validated + const task = await taskService.create(result.data); + return res.status(201).json(task); +}); +``` + +### File Upload Safety + +```typescript +// Restrict file types and sizes +const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']; +const MAX_SIZE = 5 * 1024 * 1024; // 5MB + +function validateUpload(file: UploadedFile) { + if (!ALLOWED_TYPES.includes(file.mimetype)) { + throw new ValidationError('File type not allowed'); + } + if (file.size > MAX_SIZE) { + throw new ValidationError('File too large (max 5MB)'); + } + // Don't trust the file extension — check magic bytes if critical +} +``` + +## Triaging npm audit Results + +Not all audit findings require immediate action. Use this decision tree: + +``` +npm audit reports a vulnerability +├── Severity: critical or high +│ ├── Is the vulnerable code reachable in your app? +│ │ ├── YES --> Fix immediately (update, patch, or replace the dependency) +│ │ └── NO (dev-only dep, unused code path) --> Fix soon, but not a blocker +│ └── Is a fix available? +│ ├── YES --> Update to the patched version +│ └── NO --> Check for workarounds, consider replacing the dependency, or add to allowlist with a review date +├── Severity: moderate +│ ├── Reachable in production? --> Fix in the next release cycle +│ └── Dev-only? --> Fix when convenient, track in backlog +└── Severity: low + └── Track and fix during regular dependency updates +``` + +**Key questions:** +- Is the vulnerable function actually called in your code path? +- Is the dependency a runtime dependency or dev-only? +- Is the vulnerability exploitable given your deployment context (e.g., a server-side vulnerability in a client-only app)? + +When you defer a fix, document the reason and set a review date. + +### Supply-Chain Hygiene + +`npm audit` catches known CVEs; it won't catch a malicious or typosquatted package. Also: + +- **Commit the lockfile** and install with `npm ci` (not `npm install`) in CI — reproducible builds, no silent version drift. +- **Review new dependencies before adding them** — maintenance, download counts, and whether they truly earn their place. Every dependency is attack surface (OWASP **A06: Vulnerable Components**, **LLM03: Supply Chain**). +- **Be wary of `postinstall` scripts** in unfamiliar packages — they run arbitrary code at install time. +- **Watch for typosquats** — `cross-env` vs `crossenv`, `react-dom` vs `reactdom`. + +## Rate Limiting + +```typescript +import rateLimit from 'express-rate-limit'; + +// General API rate limit +app.use('/api/', rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // 100 requests per window + standardHeaders: true, + legacyHeaders: false, +})); + +// Stricter limit for auth endpoints +app.use('/api/auth/', rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, // 10 attempts per 15 minutes +})); +``` + +## Secrets Management + +``` +.env files: + ├── .env.example → Committed (template with placeholder values) + ├── .env → NOT committed (contains real secrets) + └── .env.local → NOT committed (local overrides) + +.gitignore must include: + .env + .env.local + .env.*.local + *.pem + *.key +``` + +**Always check before committing:** +```bash +# Check for accidentally staged secrets +git diff --cached | grep -i "password\|secret\|api_key\|token" +``` + +**If a secret is ever committed, rotate it.** Deleting the line or rewriting history is not enough — assume it's compromised the moment it reaches a remote. Revoke and reissue the key first, then purge it from history. + +## Securing AI / LLM Features + +If your app calls an LLM — chatbots, summarizers, agents, RAG — it inherits a new attack surface. Map it to the [OWASP Top 10 for LLM Applications (2025)](https://genai.owasp.org/llm-top-10/): + +- **Treat all model output as untrusted input (LLM05: Improper Output Handling).** Never pass LLM output straight into `eval`, SQL, a shell, `innerHTML`, or a file path. Validate and encode it exactly as you would raw user input. +- **Assume prompts can be hijacked (LLM01: Prompt Injection).** Untrusted text in the context window — a user message, a fetched web page, a PDF — can carry instructions. The system prompt is not a security boundary; enforce permissions in code, not in the prompt. +- **Keep secrets and other users' data out of prompts (LLM02 / LLM07).** Anything in the context can be echoed back. Don't put API keys, cross-tenant data, or the full system prompt where the model can repeat it. +- **Constrain tool and agent permissions (LLM06: Excessive Agency).** Scope tools to the minimum, require confirmation for destructive or irreversible actions, and validate every tool argument. +- **Bound consumption (LLM10: Unbounded Consumption).** Cap tokens, request rate, and loop/recursion depth so a crafted input can't run up cost or hang the system. +- **Isolate retrieval data (LLM08: Vector and Embedding Weaknesses).** In RAG, treat the vector store as a trust boundary: partition embeddings per tenant so one user can't retrieve another's data, and validate documents before indexing so poisoned content can't steer answers. + +```typescript +// BAD: trusting model output as a command or as markup +const sql = await llm.generate(`Write SQL for: ${userQuestion}`); +await db.query(sql); // arbitrary query execution +container.innerHTML = await llm.reply(userMessage); // stored XSS, via the model + +// GOOD: model output is data — parse defensively, then validate, then encode +let intent; +try { + intent = CommandSchema.parse(JSON.parse(await llm.replyJson(userMessage))); +} catch { + throw new ValidationError('unexpected model output'); // JSON.parse or schema failed +} +await runAllowlistedAction(intent.action, intent.params); +container.textContent = await llm.reply(userMessage); +``` + +## Security Review Checklist + +```markdown +### Authentication +- [ ] Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12) +- [ ] Session tokens are httpOnly, secure, sameSite +- [ ] Login has rate limiting +- [ ] Password reset tokens expire + +### Authorization +- [ ] Every endpoint checks user permissions +- [ ] Users can only access their own resources +- [ ] Admin actions require admin role verification + +### Input +- [ ] All user input validated at the boundary +- [ ] SQL queries are parameterized +- [ ] HTML output is encoded/escaped +- [ ] Server-side URL fetches are allowlisted (no SSRF to internal services) + +### Data +- [ ] No secrets in code or version control +- [ ] Sensitive fields excluded from API responses +- [ ] PII encrypted at rest (if applicable) + +### Infrastructure +- [ ] Security headers configured (CSP, HSTS, etc.) +- [ ] CORS restricted to known origins +- [ ] Dependencies audited for vulnerabilities +- [ ] Error messages don't expose internals + +### Supply Chain +- [ ] Lockfile committed; CI installs with `npm ci` +- [ ] New dependencies reviewed (maintenance, downloads, postinstall scripts) + +### AI / LLM (if used) +- [ ] Model output treated as untrusted (no eval/SQL/innerHTML/shell) +- [ ] Secrets and other users' data kept out of prompts +- [ ] Tool/agent permissions scoped; destructive actions require confirmation +``` +## Common Rationalizations + +| Rationalization | Reality | +|---|---| +| "This is an internal tool, security doesn't matter" | Internal tools get compromised. Attackers target the weakest link. | +| "We'll add security later" | Security retrofitting is 10x harder than building it in. Add it now. | +| "No one would try to exploit this" | Automated scanners will find it. Security by obscurity is not security. | +| "The framework handles security" | Frameworks provide tools, not guarantees. You still need to use them correctly. | +| "It's just a prototype" | Prototypes become production. Security habits from day one. | +| "Threat modeling is overkill here" | Five minutes of "how would I attack this?" prevents the design flaws no control can patch later. | +| "It's just LLM output, it's only text" | That "text" can be a SQL statement, a script tag, or a shell command. Treat it like any untrusted input. | + +## Red Flags + +- User input passed directly to database queries, shell commands, or HTML rendering +- Secrets in source code or commit history +- API endpoints without authentication or authorization checks +- Missing CORS configuration or wildcard (`*`) origins +- No rate limiting on authentication endpoints +- Stack traces or internal errors exposed to users +- Dependencies with known critical vulnerabilities +- Server fetches user-supplied URLs without an allowlist (SSRF) +- LLM/model output passed into a query, the DOM, a shell, or `eval` +- Secrets, PII, or the full system prompt placed inside an LLM context window + +## Verification + +After implementing security-relevant code: + +- [ ] `npm audit` shows no critical or high vulnerabilities +- [ ] No secrets in source code or git history +- [ ] All user input validated at system boundaries +- [ ] Authentication and authorization checked on every protected endpoint +- [ ] Security headers present in response (check with browser DevTools) +- [ ] Error responses don't expose internal details +- [ ] Rate limiting active on auth endpoints +- [ ] Server-side URL fetches validated against an allowlist (no SSRF) +- [ ] LLM/model output validated and encoded before use (if AI features present) From 85dbf14344376946fcefc1d6665a760ba00ba2cc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Jul 2026 09:50:43 +0200 Subject: [PATCH 2/5] :paperclip: Add better planner skill and improve testing doc --- .gitignore | 3 + .opencode/skills/planner/SKILL.md | 254 ++++++++++++++---- .serena/memories/backend/core.md | 2 +- .serena/memories/common/core.md | 2 +- .serena/memories/common/testing-principles.md | 47 ---- .serena/memories/critical-info.md | 1 + .serena/memories/exporter/core.md | 1 + .serena/memories/frontend/core.md | 2 +- .serena/memories/library/core.md | 1 + .serena/memories/mcp/core.md | 1 + .serena/memories/plugins/core.md | 1 + .serena/memories/render-wasm/core.md | 1 + .serena/memories/testing.md | 149 ++++++++++ 13 files changed, 356 insertions(+), 109 deletions(-) delete mode 100644 .serena/memories/common/testing-principles.md create mode 100644 .serena/memories/testing.md diff --git a/.gitignore b/.gitignore index 7c3309ac20..dfdb8bae03 100644 --- a/.gitignore +++ b/.gitignore @@ -99,5 +99,8 @@ opencode.json /.playwright-mcp /.devenv/mcp/ /opencode.json +/.opencode/plans +/.opencode/reports +/.opencode/prompts /.codex/ /tools/__pycache__ diff --git a/.opencode/skills/planner/SKILL.md b/.opencode/skills/planner/SKILL.md index 7886dda85f..d1802652e2 100644 --- a/.opencode/skills/planner/SKILL.md +++ b/.opencode/skills/planner/SKILL.md @@ -1,6 +1,6 @@ --- name: planner -description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to plans/YYYY-MM-DD-.md only when the calling agent has write permission. +description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to .opencode/plans/YYYY-MM-DD-<title>.md. --- # Planner @@ -17,7 +17,7 @@ or modifies code. names, and test strategy. - The user asks "how would I implement X?" or "what's involved in fixing Y?". - The user is about to start non-trivial work and wants a bite-sized task - breakdown (DRY, YAGNI, TDD, frequent commits). + breakdown. Do **not** use this skill to actually implement anything — it is read-only. @@ -29,13 +29,8 @@ modify code. You help users understand the codebase, design solutions, and create detailed implementation plans that other agents or developers can execute. Document -everything they need to know: which files to touch for each task, code, tests, -docs they might need to check, and how to verify it. Give them the whole plan -as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits. - -Assume the implementer is a skilled developer, but knows almost nothing about -our toolset or problem domain. Assume they don't know good test design very -well. +everything they need to know: which files to touch for each task, code patterns, +tests, and how to verify correctness. Apply DRY and KISS principles. Do **not** suggest commit messages or commit names anywhere in your plans or responses — committing is the developer's responsibility. @@ -44,92 +39,233 @@ responses — committing is the developer's responsibility. Before drafting any plan, work through the project's own guidance: -1. Read `AGENTS.md` (root) for the project-level rules. -2. Read `.serena/memories/critical-info.md` (or the equivalent entry point) to - identify which modules are affected. +1. Read `critical-info` (`.serena/memories/critical-info.md`) — the entry point + that describes the monorepo structure and module dependency graph. +2. From `critical-info`, identify which modules your task affects. 3. Read each affected module's core memory, e.g. `mem:frontend/core`, `mem:backend/core`, `mem:common/core`, `mem:exporter/core`, `mem:render-wasm/core`. Follow `mem:` references deeper as needed. -4. For frontend/backend work, check the relevant section's notes on lint, - format, and test commands so the plan can include them. +4. For each affected module, note its lint, format, and test commands so the + plan can include concrete verification steps. Skipping this step is the #1 cause of incorrect or incomplete plans. +## The Planning Process + +### Phase 1: Architecture Analysis + +1. Read the spec, requirements, or feature request. +2. Analyze the codebase architecture and identify affected modules. +3. Read project conventions (starting with `critical-info` and module core + memories) before drafting. +4. Map dependencies between components (see the dependency graph in + `critical-info`). +5. Identify risks, edge cases, performance implications, and breaking changes. + +### Phase 2: Task Breakdown + +Implementation order follows the monorepo's dependency graph: +`frontend -> common`, `backend -> common`, `exporter -> common`, +`frontend -> render-wasm`. Build shared foundations first, then layer +consumers on top. + +#### Slice Vertically + +Instead of building all of common, then all of backend, then all of frontend — +build one complete feature path at a time: + +``` +Task 1: common data types + schema ← foundation +Task 2: backend RPC handler + persistence +Task 3: frontend UI component + API integration +``` + +Each vertical slice delivers working, testable functionality. + +#### Write Tasks + +Each task follows this structure: + +```markdown +## Task [N]: [Short descriptive title] + +**Description:** One paragraph explaining what this task accomplishes. + +**Acceptance criteria:** +- [ ] [Specific, testable condition] +- [ ] [Specific, testable condition] + +**Verification:** +- [ ] Tests pass (module-specific test command) +- [ ] Lint/formatter passes (module-specific check command) + +**Dependencies:** [Task numbers this depends on, or "None"] + +**Files likely touched:** +- `path/to/file.clj` +- `path/to/file_test.clj` +``` + +Replace "module-specific test command" with the actual commands for the module +(e.g. `clojure -M:dev:test` for backend/common, `npx shadow-cljs compile test && npx karma start` for frontend, +or the commands noted in the module's core memory). + +#### Estimate Scope + +| Size | Files | Scope | +|------|-------|-------| +| **XS** | 1 | Single function, config change, or schema tweak | +| **S** | 1-2 | One handler or component method | +| **M** | 3-5 | One vertical feature slice | +| **L** | 5-8 | Multi-component feature | +| **XL** | 8+ | **Too large — break it down further** | + +If a task is L or larger, break it into smaller tasks. Agents perform best on +S and M tasks. + +**When to break a task down further:** +- It would take more than one focused session +- You cannot describe the acceptance criteria in 3 or fewer bullet points +- It touches two or more independent subsystems +- You find yourself writing "and" in the task title (a sign it is two tasks) + +#### Order and Checkpoints + +Arrange tasks so that: + +1. Dependencies are satisfied (build foundation first) +2. Each task leaves the system in a working state +3. Verification checkpoints occur after every 2-3 tasks +4. High-risk tasks are early (fail fast) + +Add explicit checkpoints with the relevant module commands: + +```markdown +## Checkpoint: After Tasks 1-3 +- [ ] All tests pass (module-specific command) +- [ ] Lint/format passes (module-specific command) +- [ ] Core flow works end-to-end +- [ ] Review with human before proceeding +``` + ## Requirements - Analyze the codebase architecture and identify affected modules. -- Read `AGENTS.md` and the memory system conventions before drafting. +- Read project conventions before drafting (start with `critical-info` and + affected module core memories). - Break down complex features or bugs into atomic, actionable steps. - Propose solutions with clear rationale, trade-offs, and sequencing. - Identify risks, edge cases, performance implications, and breaking changes. - Apply DRY and KISS principles to the proposed implementation. - Define a testing strategy aligned with each affected module's tooling. +- Every task must have acceptance criteria and verification steps. +- Checkpoints must exist between major phases. ## Constraints - You are **analysis-only** — never create, edit, or delete source code. -- The only file write you may attempt is the plan itself, and only when the - calling agent has write permission (see "Plan Output"). If the write is - denied, deliver the plan in the response and move on. +- The only file write you may attempt is the plan itself, saved to + `.opencode/plans/`. - You do **not** run builds, tests, linters, or any commands that modify state. - You do **not** create git commits or interact with version control. -- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`, - `find`, `cat`, `bat`). +- You do **not** execute shell commands beyond read-only searches. - Your output is a structured plan or analysis, ready for handoff to an engineer agent or developer. -## Plan Output +## Output Format The plan is always delivered in the response so the user sees it regardless of which agent is running the skill. -Persistence is a **separate, best-effort step** that only runs when the -calling agent has `edit` write permission: +Additionally, save the plan to: -- **Has write permission** (e.g. `build`, `general`, `engineer`): in addition - to the in-response plan, save the plan to: +``` +.opencode/plans/YYYY-MM-DD-<plan-one-line-title>.md +``` - ``` - plans/YYYY-MM-DD-<plan-one-line-title>.md - ``` +Use today's date in the user's local timezone. The `<plan-one-line-title>` +slug is lowercase, hyphen-separated, and a short summary of the task +(e.g. `add-batch-get-profiles-for-file-comments`). Create the +`.opencode/plans/` directory if it does not exist. - Use today's date in the user's local timezone. The `<plan-one-line-title>` - slug is lowercase, hyphen-separated, and a short summary of the task - (e.g. `add-batch-get-profiles-for-file-comments`). Create the `plans/` - directory if it does not exist. +Always attempt the write. If the user explicitly provides a target file path, +use that path instead of the default. -- **No write permission** (e.g. the built-in `plan` agent, which denies - `edit`): do not attempt to write the file — the write tool will be - rejected. Just deliver the plan in the response. The user can copy it into - `plans/...` manually if they want it persisted. +### Plan Document Template -If the user explicitly provides a target file path, use that path instead of -the default `plans/YYYY-MM-DD-<slug>.md` (still subject to write permission). +```markdown +# Plan: [Feature/Project Name] -How to detect write permission: try the write. If it is denied, treat the -plan as response-only and proceed — do not retry, do not ask the user, and do -not mention the failed write in the response. +## Context +[One paragraph: what is the problem or feature request? Why is it needed?] -## Output Format +## Affected Modules +[Which modules of the monorepo are involved? Reference module paths and any +`mem:` memories that were consulted.] -Structure the plan as: +## Architecture Decisions +- [Key decision 1 and rationale] +- [Key decision 2 and rationale] -1. **Context** — What is the problem or feature request? Why is it needed? -2. **Affected modules** — Which parts of the codebase are involved? Reference - module paths and any `mem:` memories that were consulted. -3. **Approach** — Step-by-step implementation plan with file paths, function - names, and code shape where applicable. Group steps into atomic, ordered - tasks. -4. **Risks & considerations** — Edge cases, performance implications, - breaking changes, migration concerns, security implications. -5. **Testing strategy** — How to verify the implementation works correctly: - which test commands to run per module, what cases to cover, manual - verification steps, lint/format checks. +## Risks & Considerations +[Edge cases, performance implications, breaking changes, migration concerns, +security implications.] -Each step in **Approach** should be small enough to be reviewed and committed -independently. Cite exact file paths (`path/to/file.ext:line` when useful) so -the implementer can navigate directly. +## Approach +[Step-by-step implementation plan with file paths, function names, and code +shape where applicable. Group steps into atomic, ordered tasks.] + +## Task List + +### Phase 1: Foundation +- [ ] Task 1: ... +- [ ] Task 2: ... + +### Checkpoint: Phase 1 +- [ ] Tests pass, lint/formatter clean (module-specific commands) + +### Phase 2: Core Features +- [ ] Task 3: ... +- [ ] Task 4: ... + +### Checkpoint: Phase 2 +- [ ] End-to-end flow works + +### Phase 3: Polish +- [ ] Task 5: ... +- [ ] Task 6: ... + +### Checkpoint: Complete +- [ ] All acceptance criteria met +- [ ] Ready for review + +## Testing Strategy +[How to verify: which test commands to run per module, what cases to cover, +manual verification steps, lint/format checks. Consult each module's core +memory for the exact commands.] + +## Parallelization Opportunities +- **Safe to parallelize:** Independent feature slices across separate + modules, tests for already-implemented features +- **Must be sequential:** Shared common schema changes, database migrations +- **Needs coordination:** Features that share a contract (define the contract + first, then parallelize) + +## Open Questions +- [Question needing human input] +``` When the plan is purely analytical (e.g. a code review or feasibility study -with no implementation), skip the **Approach** section and lead with -**Findings** instead, keeping the rest of the structure. +with no implementation), skip the **Approach** and **Task List** sections and +lead with **Findings** instead, keeping the rest of the structure. + +## Verification Checklist + +Before starting implementation, confirm: + +- [ ] Every task has acceptance criteria +- [ ] Every task has a verification step +- [ ] Task dependencies are identified and ordered correctly +- [ ] No task touches more than ~5 files +- [ ] Checkpoints exist between major phases +- [ ] The human has reviewed and approved the plan diff --git a/.serena/memories/backend/core.md b/.serena/memories/backend/core.md index f416b78cf8..12a9bcda25 100644 --- a/.serena/memories/backend/core.md +++ b/.serena/memories/backend/core.md @@ -106,4 +106,4 @@ IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory. * **Coverage:** If code is added or modified in `src/`, corresponding tests in `test/backend_tests/` must be added or updated. * **Isolated run:** `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace. * **Regression run:** `clojure -M:dev:test` to ensure no regressions in related functional areas. -* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:common/testing-principles`. +* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`. diff --git a/.serena/memories/common/core.md b/.serena/memories/common/core.md index 23fbe20198..6ae8f084e8 100644 --- a/.serena/memories/common/core.md +++ b/.serena/memories/common/core.md @@ -49,7 +49,7 @@ Components, variants, and debugging: Text and tests: - Shared text data conversion, DraftJS compatibility, modern text content, and derived position data: `mem:common/text-subtleties`. - Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/testing`. -- Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:common/testing-principles`. +- Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`. ## Areas without focused memories diff --git a/.serena/memories/common/testing-principles.md b/.serena/memories/common/testing-principles.md deleted file mode 100644 index 860852393b..0000000000 --- a/.serena/memories/common/testing-principles.md +++ /dev/null @@ -1,47 +0,0 @@ -# Testing Principles (Cross-Cutting) - -Shared testing principles for all modules (backend, frontend, common, render-wasm, plugins). Module-specific commands and helpers are in each module's own testing memory. - -## Core Principles - -- **Test State, Not Interactions** — assert on outcomes, not method calls; survives refactoring -- **DAMP over DRY** — tests are specifications; duplication OK if each test is self-contained and readable -- **Prefer Real Implementations** — hierarchy: Real > Fake > Stub > Mock; mock only at boundaries (network, filesystem, email) -- **Arrange-Act-Assert** — every test: setup / action / verify -- **One Assertion Per Concept** — each test verifies one behavior; split compound assertions -- **Descriptive Test Names** — names read like specifications - -## Anti-Patterns - -- Testing implementation details → breaks on refactor -- Flaky tests (timing, order-dependent) → erode trust -- Mocking everything → tests pass, production breaks -- No test isolation → pass individually, fail together -- Testing framework code → waste of time -- Snapshot abuse → nobody reviews, break on any change - -## Verification Checklist - -After completing any implementation: -- Every new behavior has a corresponding test -- All tests pass for touched modules -- Bug fixes include a reproduction test that failed before the fix -- Test names describe the behavior being verified -- No tests were skipped or disabled - -## Red Flags - -- Writing code without corresponding tests -- Tests that pass on the first run (may not be testing what you think) -- Bug fixes without reproduction tests -- Tests that test framework behavior instead of app behavior -- Skipping tests to make the suite pass - -## Rationalizations - -- "I'll write tests after" → you won't; they'll test implementation not behavior -- "Too simple to test" → simple gets complicated; test documents expected behavior -- "Tests slow me down" → they speed up every future change -- "I tested manually" → manual testing doesn't persist -- "Code is self-explanatory" → tests ARE the specification -- "Just a prototype" → prototypes become production; test debt accumulates diff --git a/.serena/memories/critical-info.md b/.serena/memories/critical-info.md index 1027829bcf..d291c81bf9 100644 --- a/.serena/memories/critical-info.md +++ b/.serena/memories/critical-info.md @@ -6,6 +6,7 @@ You are working on the GitHub project `penpot/penpot`, a monorepo. - A section's top-level memory is `<section>/core`. When a section is relevant, read the core memory before focused memories. - Edits/stale refs/duplication cleanup: `mem:memory-maintenance`. +- Cross-cutting testing principles, TDD workflow, and anti-patterns: `mem:testing`. # Development workflow diff --git a/.serena/memories/exporter/core.md b/.serena/memories/exporter/core.md index da61317d63..3bcf784f49 100644 --- a/.serena/memories/exporter/core.md +++ b/.serena/memories/exporter/core.md @@ -7,6 +7,7 @@ - Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`. - From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`. - Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`. +- Cross-cutting testing principles and anti-patterns: `mem:testing`. ## HTTP and browser pool diff --git a/.serena/memories/frontend/core.md b/.serena/memories/frontend/core.md index b890de6770..5f06f8cfb0 100644 --- a/.serena/memories/frontend/core.md +++ b/.serena/memories/frontend/core.md @@ -52,7 +52,7 @@ Diagnostics and validation: - Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`. - Runtime crash recovery: `mem:frontend/handling-crashes`. - Tests and live verification: `mem:frontend/testing`. -- Cross-cutting testing principles and anti-patterns: `mem:common/testing-principles`. +- Cross-cutting testing principles and anti-patterns: `mem:testing`. - Real pointer/keyboard gesture reproduction: `mem:frontend/playwright-gestures`. ## Areas without focused memories diff --git a/.serena/memories/library/core.md b/.serena/memories/library/core.md index dec9347c6f..7ff57c74b2 100644 --- a/.serena/memories/library/core.md +++ b/.serena/memories/library/core.md @@ -7,6 +7,7 @@ - Source: `library/src/`; tests: `library/test/`; experimentation/docs: `playground/`, `docs/`; config: `shadow-cljs.edn`, `deps.edn`, `package.json`. - From `library/`: build `pnpm run build`; bundle helper `pnpm run build:bundle` or `./scripts/build`; tests `pnpm run test`; watch `pnpm run watch` / `pnpm run watch:test`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`. - When changing file-format construction or export behavior in `common/`, consider whether `@penpot/library` should be tested because it constructs Penpot files outside the app UI. +- Cross-cutting testing principles and anti-patterns: `mem:testing`. ## JS API and builder state diff --git a/.serena/memories/mcp/core.md b/.serena/memories/mcp/core.md index 772f7a3949..7aeb784470 100644 --- a/.serena/memories/mcp/core.md +++ b/.serena/memories/mcp/core.md @@ -85,6 +85,7 @@ From the `mcp/` directory, run * `pnpm run build` to test the build of all packages * `pnpm run fmt` to apply the auto-formatter +* Cross-cutting testing principles and anti-patterns: `mem:testing`. ## Devenv plugin/server wiring diff --git a/.serena/memories/plugins/core.md b/.serena/memories/plugins/core.md index ea51e5dad6..b042c34aa8 100644 --- a/.serena/memories/plugins/core.md +++ b/.serena/memories/plugins/core.md @@ -13,6 +13,7 @@ - From `plugins/`: install `pnpm -r install`; runtime dev server `pnpm run start` or `pnpm run start:app:runtime`; sample plugin `pnpm run start:plugin:<name>`; build runtime `pnpm run build:runtime`; build plugins `pnpm run build:plugins`; lint `pnpm run lint`; format `pnpm run format:check` / `pnpm run format`; tests `pnpm run test`; e2e `pnpm run test:e2e`. - If a change affects public Plugin API types or runtime, update `plugins/CHANGELOG.md`. Prefix type/signature entries with `**plugin-types:**`; runtime behavior entries with `**plugin-runtime:**`. +- Cross-cutting testing principles and anti-patterns: `mem:testing`. - JS Plugin API behavior inside Penpot app: `mem:frontend/plugin-api-to-cljs-binding`; TS declarations are not runtime code; many API objects are CLJS proxies in `frontend/src/app/plugins/*.cljs`. ## Sandbox and global cleanup diff --git a/.serena/memories/render-wasm/core.md b/.serena/memories/render-wasm/core.md index 30734d2b0d..17345b7a7d 100644 --- a/.serena/memories/render-wasm/core.md +++ b/.serena/memories/render-wasm/core.md @@ -26,6 +26,7 @@ From `render-wasm/`: - Build/copy frontend artifacts: `./build`. - Watch rebuild: `./watch`. - Rust tests: `./test` or `cargo test <name>`. +- Cross-cutting testing principles and anti-patterns: `mem:testing`. - Lint: `./lint`. - Format check: `cargo fmt --check`. diff --git a/.serena/memories/testing.md b/.serena/memories/testing.md new file mode 100644 index 0000000000..2f9f4c9131 --- /dev/null +++ b/.serena/memories/testing.md @@ -0,0 +1,149 @@ +# Testing + +## Overview + +Tests are proof that code works. Every behavior change needs a test. + +Testing in this monorepo varies by module. Each module has its own test +commands, helpers, runner registration requirements, and conventions. This +memory covers cross-cutting testing principles. For module-specific commands +and helpers, consult: + +- `mem:common/testing` — CLJC unit tests (JVM + JS), test helpers, fixture + builders, production-path change helpers +- `mem:frontend/testing` — CLJS unit tests, Playwright E2E integration tests, + live browser verification via nREPL +- Backend — JVM `clojure.test` under `backend/test/`; see `mem:backend/core` + +## When to Use + +- Implementing new logic or behavior +- Fixing any bug (reproduction test required) +- Modifying existing functionality +- Adding edge case handling + +**When NOT to use:** Pure configuration changes, documentation updates, or +static content changes with no behavioral impact. + +## TDD: Recommended Workflow + +Write a failing test before writing the code that makes it pass. For bug fixes, +reproduce the bug with a test before attempting a fix. + +When TDD isn't practical (exploratory work, tight coupling to unknown APIs), +still write tests before considering the work complete. + +``` + RED GREEN REFACTOR + Write a test Write minimal code Clean up the + that fails ──→ to make it pass ──→ implementation ──→ (repeat) + │ │ │ + ▼ ▼ ▼ + Test FAILS Test PASSES Tests still PASS +``` + +- **RED** — Write the test first. It must fail. A test that passes immediately + proves nothing. +- **GREEN** — Write the minimum code to make the test pass. Don't over-engineer. +- **REFACTOR** — With tests green, improve the code without changing behavior: + extract shared logic, improve naming, remove duplication. Run tests after + every step. + +## The Prove-It Pattern (Bug Fixes) + +When a bug is reported, **do not start by trying to fix it.** Start by writing +a test that reproduces it: + +1. Write a test that demonstrates the bug +2. Confirm the test FAILS (proving the bug exists) +3. Implement the fix +4. Confirm the test PASSES (proving the fix works) +5. Run the full test suite for the module (no regressions) + +## Core Principles + +- **Test State, Not Interactions** — assert on outcomes, not method calls; + survives refactoring +- **DAMP over DRY** — tests are specifications; duplication is OK if each test + is self-contained and readable. A test should tell a complete story without + requiring the reader to trace through shared helpers. +- **Prefer Real Implementations** — hierarchy: Real > Fake > Stub > Mock; + mock only at boundaries (network, RPC, filesystem, email) +- **Arrange-Act-Assert** — every test: setup / action / verify +- **One Assertion Per Concept** — each test verifies one behavior; split + compound assertions +- **Descriptive Test Names** — names read like specifications + +## Prefer Real Implementations Over Mocks + +Work down this list: + +1. **Real implementation** — Test the actual code with real collaborators. + Highest confidence. +2. **Fake** — A simplified but functional in-memory implementation (e.g. + atom/dict-backed store instead of a real database). +3. **Stub** — Returns canned data. Use when the collaborator's logic is + irrelevant. +4. **Mock** — Last resort, only at boundaries. Use only when verifying + interaction with an external system that cannot be faked. + +**Rule of thumb:** If you can write a fake or use the real implementation, do +that. If you find yourself asserting on call counts or invocation order, ask +whether a fake would be clearer. + +## Fixtures over Manual Setup + +Use fixture/`beforeEach` mechanisms for shared setup and teardown. Each test +should own its state so tests don't interfere with each other. Shorter-scope +fixtures (`:each` / per-test) are preferred; longer-scope fixtures (`:once` / +suite-level) are only for expensive, immutable shared setup. + +## Parametrized Tests + +Use your test framework's parametrize/table-driven mechanism to test multiple +scenarios with a single test body. Keeps tests concise and surfaces all cases +at a glance. + +## Test Pyramid + +``` + ╱╲ + ╱ ╲ E2E (few) + ╱ ╲ Full flows, real browser/server + ╱──────╲ + ╱ ╲ Integration (some) + ╱ ╲ Cross-module, test DB + ╱────────────╲ + ╱ ╲ Unit (most) + ╱ ╲ Pure logic, fast + ╱──────────────────╲ +``` + +Prefer unit tests for pure logic. Reach for integration/E2E tests when covering +RPC handlers, database queries, or full user flows. In the frontend, Playwright +E2E tests should not be added unless explicitly requested. + +## Anti-Patterns + +| Anti-Pattern | Problem | Fix | +|---|---|---| +| Testing implementation details | Breaks on refactor | Test inputs/outputs | +| Flaky tests (timing, order-dependent) | Erodes trust | Deterministic assertions, isolate state | +| Mocking everything | Tests pass, production breaks | Prefer real implementations or fakes | +| No test isolation | Pass individually, fail together | Per-test state fixtures | +| Testing framework/platform code | Wastes time | Only test YOUR code | +| Snapshot abuse | Nobody reviews, break on any change | Focused assertions | +| Skipping tests to make suite pass | Hides real failures | Fix the test or fix the code | + +## Verification Checklist + +After completing any implementation: + +- [ ] Every new behavior has a corresponding test +- [ ] All tests pass for touched modules +- [ ] Bug fixes include a reproduction test that failed before the fix +- [ ] Test names describe the behavior being verified +- [ ] No tests were skipped or disabled +- [ ] Lint/formatter passes for touched modules +- [ ] New test files registered in the module's runner/entrypoint (see module + testing memory) From e48f37498429da15fb72df6a9152f12643544d18 Mon Sep 17 00:00:00 2001 From: Andrey Antukh <niwi@niwi.nz> Date: Wed, 15 Jul 2026 17:32:16 +0200 Subject: [PATCH 3/5] :arrow_up: Update opencode on devenv dockerfile --- docker/devenv/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 1fd9a7b382..b965526cbe 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -66,7 +66,7 @@ RUN set -eux; \ FROM base AS setup-opencode -ENV OPENCODE_VERSION=1.17.16 +ENV OPENCODE_VERSION=1.18.1 RUN set -ex; \ ARCH="$(dpkg --print-architecture)"; \ From e498dd238293bcb1218e603809d82502dacd2d95 Mon Sep 17 00:00:00 2001 From: Andrey Antukh <niwi@niwi.nz> Date: Wed, 15 Jul 2026 21:01:33 +0200 Subject: [PATCH 4/5] :paperclip: Update commiter opencode agent --- .opencode/agents/commiter.md | 95 ++++++------------------------------ 1 file changed, 14 insertions(+), 81 deletions(-) diff --git a/.opencode/agents/commiter.md b/.opencode/agents/commiter.md index 66b56d1041..a5e128e4d1 100644 --- a/.opencode/agents/commiter.md +++ b/.opencode/agents/commiter.md @@ -6,7 +6,6 @@ permission: read: allow glob: allow grep: allow - list: allow edit: deny webfetch: deny websearch: deny @@ -14,66 +13,9 @@ permission: skill: deny lsp: deny todowrite: deny - question: allow + question: deny external_directory: deny - bash: - # Broad read-side: any non-write git query - "git status*": allow - "git log*": allow - "git diff*": allow - "git show*": allow - "git rev-parse*": allow - "git branch*": allow - "git remote -v*": allow - "git config --get*": allow - - # Commit flow: staged, explicit paths only. `git commit*` (no space) - # also covers `git commit --amend`. `git add -*` overrides the allow - # below to block flag-driven bulk adds (`-A`, `--all`, `-u`, ...). - "git add *": allow - "git commit*": allow - "git add -*": deny - - # Read-only filesystem helpers used in the commit flow - "cat *": allow - "head *": allow - "tail *": allow - "wc *": allow - "date *": allow - - # Dangerous: deny outright - "rm *": deny - "rmdir *": deny - "mv *": deny - "cp *": deny - "dd *": deny - "chmod *": deny - "chown *": deny - "sudo *": deny - "git push*": deny - "git clean*": deny - "git reset*": deny - "git checkout*": deny - "git restore*": deny - "git config --global*": deny - "curl *": deny - "wget *": deny - "ssh *": deny - "scp *": deny - "eval *": deny - - # Risky-but-sometimes-needed: ask the user - "git stash*": ask - "git rebase*": ask - "git merge*": ask - "git tag*": ask - "git fetch*": ask - "git pull*": ask - # Note: `git config <anything-other-than-(--get|--global)>` falls - # through to the `*` catch-all below and is asked. - - # Safety net - "*": ask + bash: allow --- ## Role @@ -91,13 +33,12 @@ exactly — do not improvise the format and do not restate its contents here. ## Pre-commit Workflow -1. Run `git status` to inspect the working tree. If there are unstaged or - untracked changes unrelated to the user's request, STOP and ask how to - handle them. Do not silently include unrelated work in the commit. -2. Run `git diff --staged` (or `git diff` for unstaged changes) and review the - content. If you see secrets (API keys, tokens, passwords, private keys, - `.env` values), debug prints, or anything that does not match the user's - stated intent, STOP and tell the user before committing. +1. **Stage the files** specified by the calling agent. Do not ask for + confirmation — the calling agent knows exactly which files to commit. +2. Run `git diff --staged` to review the content. If you see secrets (API + keys, tokens, passwords, private keys, `.env` values), debug prints, or + anything that does not match the stated intent, STOP and tell the user + before committing. 3. Following the format in the doc, draft the message and run `git commit -m "<subject>" -m "<body>"` (or `git commit -F -` if the body has unusual characters). The `AI-assisted-by` trailer value is provided by the @@ -105,18 +46,10 @@ exactly — do not improvise the format and do not restate its contents here. ## Constraints -- Do not push. Pushing is a separate workflow handled by the user (the - permission set also denies `git push*`). -- Do not run `git reset*`, `git checkout*`, `git restore*`, `git clean*`, or - `rm*` — the permission set denies these outright. If staged work needs to be - discarded, ask the user to do it. +- Do not push. Pushing is a separate workflow handled by the user. +- Do not run `git reset`, `git checkout`, `git restore`, `git clean`, or `rm` — these are destructive operations. - Do not pass `--author`. Author identity comes from the local git config. - Never guess or hallucinate a name or email. -- Do not amend a commit you did not create in this session, unless the user - explicitly asks. `git commit --amend` rewrites history and is irreversible - once pushed. -- Do not bypass pre-commit hooks (`--no-verify`) unless the user explicitly - asks, and call out the deviation in your response. -- If the user asks for something that conflicts with these rules, follow the - user's request and explain the deviation in your response. Do not silently - override the format. +- Do not amend a commit you did not create in this session, unless the user explicitly asks. +- Do not bypass pre-commit hooks (`--no-verify`) unless the user explicitly asks. +- Do not add untracked files that were not created in this session. +- Do not ask questions. The calling agent provides all necessary information. If something is unclear, proceed with what you know and note any assumptions in your response. From b3105e6b8226a1313d7f99d25b465f7447e99465 Mon Sep 17 00:00:00 2001 From: Andrey Antukh <niwi@niwi.nz> Date: Wed, 15 Jul 2026 21:01:55 +0200 Subject: [PATCH 5/5] :rewind: Backport github workflows from develop --- .github/workflows/auto-label.yml | 76 ++++++++++++++++++++++++++++++ .github/workflows/build-bundle.yml | 4 ++ .github/workflows/build-docker.yml | 4 ++ .github/workflows/tests-mcp.yml | 2 +- 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/auto-label.yml diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 0000000000..27ae0f37a4 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,76 @@ +name: Auto Label and Add to Project + +on: + issues: + types: [opened] + pull_request: + types: [opened] + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App token + id: triage-app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.TRIAGE_APP_ID }} + private-key: ${{ secrets.TRIAGE_APP_PRIVATE_KEY }} + owner: penpot + + - name: Process Issue or PR + uses: actions/github-script@v7 + with: + github-token: ${{ steps.triage-app-token.outputs.token }} + script: | + // === 1. CONFIGURATION === + const PROJECT_NUMBER = 8; // <--- Replace with your project board number + const IS_ORG = true; // <--- Set to false if this is a personal project, true if an organization + const OWNER = context.repo.owner; + const REPO = context.repo.repo; + + const issueNumber = context.issue.number; + const isPR = !!context.payload.pull_request; + const contentId = isPR ? context.payload.pull_request.node_id : context.payload.issue.node_id; + + // Define your labels here + const labelToApply = 'needs triage'; + + // === 2. APPLY THE LABEL === + console.log(`Applying label "${labelToApply}" to ${isPR ? 'PR' : 'Issue'} #${issueNumber}...`); + await github.rest.issues.addLabels({ + issue_number: issueNumber, + owner: OWNER, + repo: REPO, + labels: [labelToApply] + }); + + // === 3. ADD TO PROJECT BOARD === + console.log(`Fetching Project #${PROJECT_NUMBER} ID...`); + const projectQuery = ` + query($owner: String!, $number: Int!) { + ${IS_ORG ? 'organization' : 'user'}(login: $owner) { + projectV2(number: $number) { + id + } + } + } + `; + + const projectRes = await github.graphql(projectQuery, { owner: OWNER, number: PROJECT_NUMBER }); + const projectId = IS_ORG ? projectRes.organization.projectV2.id : projectRes.user.projectV2.id; + + console.log(`Adding item to project board...`); + const addToProjectMutation = ` + mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { + item { + id + } + } + } + `; + + await github.graphql(addToProjectMutation, { projectId, contentId }); + console.log("Automation successfully completed!"); + diff --git a/.github/workflows/build-bundle.yml b/.github/workflows/build-bundle.yml index 8f8b40007a..e31382ccd4 100644 --- a/.github/workflows/build-bundle.yml +++ b/.github/workflows/build-bundle.yml @@ -37,6 +37,10 @@ on: required: false default: 'yes' +concurrency: + group: ${{ github.workflow }}-${{ inputs.gh_ref }} + cancel-in-progress: true + jobs: build-bundle: name: Build and Upload Penpot Bundle diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 18ac6aec9f..0972f1e25d 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -16,6 +16,10 @@ on: required: true default: 'develop' +concurrency: + group: ${{ github.workflow }}-${{ inputs.gh_ref }} + cancel-in-progress: true + jobs: build-and-push: name: Build and Push Penpot Docker Images diff --git a/.github/workflows/tests-mcp.yml b/.github/workflows/tests-mcp.yml index 9e622ca83a..6f8eadcadd 100644 --- a/.github/workflows/tests-mcp.yml +++ b/.github/workflows/tests-mcp.yml @@ -49,4 +49,4 @@ jobs: - name: Tests working-directory: ./mcp run: | - pnpm -r run test; + pnpm run test;