From 2c24991f884d903cc2e987f8d4195a938f05794d Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 3 May 2026 01:35:05 +0700 Subject: [PATCH] 690503:0135 Update workflow #01 --- .agents/skills/diagnose/SKILL.md | 117 ++++++ .../diagnose/scripts/hitl-loop.template.sh | 41 ++ .agents/skills/grill-with-docs/ADR-FORMAT.md | 47 +++ .../skills/grill-with-docs/CONTEXT-FORMAT.md | 77 ++++ .agents/skills/grill-with-docs/SKILL.md | 88 ++++ .../skills/setup-matt-pocock-skills/SKILL.md | 121 ++++++ .../skills/setup-matt-pocock-skills/domain.md | 51 +++ .../issue-tracker-github.md | 22 + .../issue-tracker-gitlab.md | 23 ++ .../issue-tracker-local.md | 19 + .../setup-matt-pocock-skills/triage-labels.md | 15 + .kilocode/skills/diagnose/SKILL.md | 117 ++++++ .../diagnose/scripts/hitl-loop.template.sh | 41 ++ .../skills/grill-with-docs/ADR-FORMAT.md | 47 +++ .../skills/grill-with-docs/CONTEXT-FORMAT.md | 77 ++++ .kilocode/skills/grill-with-docs/SKILL.md | 88 ++++ .../skills/setup-matt-pocock-skills/SKILL.md | 121 ++++++ .../skills/setup-matt-pocock-skills/domain.md | 51 +++ .../issue-tracker-github.md | 22 + .../issue-tracker-gitlab.md | 23 ++ .../issue-tracker-local.md | 19 + .../setup-matt-pocock-skills/triage-labels.md | 15 + .qwen/skills/diagnose/SKILL.md | 117 ++++++ .../diagnose/scripts/hitl-loop.template.sh | 41 ++ .qwen/skills/grill-with-docs/ADR-FORMAT.md | 47 +++ .../skills/grill-with-docs/CONTEXT-FORMAT.md | 77 ++++ .qwen/skills/grill-with-docs/SKILL.md | 88 ++++ .../skills/setup-matt-pocock-skills/SKILL.md | 121 ++++++ .../skills/setup-matt-pocock-skills/domain.md | 51 +++ .../issue-tracker-github.md | 22 + .../issue-tracker-gitlab.md | 23 ++ .../issue-tracker-local.md | 19 + .../setup-matt-pocock-skills/triage-labels.md | 15 + .windsurf/skills/diagnose/SKILL.md | 117 ++++++ .../diagnose/scripts/hitl-loop.template.sh | 41 ++ .../skills/grill-with-docs/ADR-FORMAT.md | 47 +++ .../skills/grill-with-docs/CONTEXT-FORMAT.md | 77 ++++ .windsurf/skills/grill-with-docs/SKILL.md | 88 ++++ .../skills/setup-matt-pocock-skills/SKILL.md | 121 ++++++ .../skills/setup-matt-pocock-skills/domain.md | 51 +++ .../issue-tracker-github.md | 22 + .../issue-tracker-gitlab.md | 23 ++ .../issue-tracker-local.md | 19 + .../setup-matt-pocock-skills/triage-labels.md | 15 + .../workflows/setup-matt-pocock-skills.md | 0 .windsurf/workflows/speckit-plan.md | 0 AGENTS.md | 16 + .../correspondence/correspondence.service.ts | 14 +- .../dto/workflow-history-item.dto.ts | 2 + .../dto/workflow-transition.dto.ts | 13 + .../entities/workflow-history.entity.ts | 11 + .../entities/workflow-instance.entity.ts | 11 + .../guards/workflow-transition.guard.spec.ts | 129 +++++- .../guards/workflow-transition.guard.ts | 47 ++- .../workflow-engine.controller.ts | 19 +- .../workflow-engine/workflow-engine.module.ts | 20 +- .../workflow-engine.service.spec.ts | 322 ++++++++++++++- .../workflow-engine.service.ts | 207 +++++++++- .../workflow-event.processor.spec.ts | 165 ++++++++ .../workflow-event.processor.ts | 133 ++++++ .../workflow-engine/workflow-event.service.ts | 94 ++--- docs/agents/domain.md | 41 ++ docs/agents/issue-tracker.md | 23 ++ docs/agents/triage-labels.md | 15 + .../doc-control/workflows/[id]/edit/page.tsx | 4 +- .../__tests__/file-preview-modal.test.tsx | 145 +++++++ .../workflows/__tests__/dsl-editor.test.tsx | 113 +++++ frontend/components/workflows/dsl-editor.tsx | 7 +- frontend/hooks/use-workflow-action.ts | 10 +- frontend/hooks/use-workflows.ts | 8 + .../lib/services/workflow-engine.service.ts | 17 + .../workflow-engine/workflow-engine.dto.ts | 3 + skills-lock.json | 18 + .../checklists/requirements.md | 45 ++ .../contracts/workflow-definitions.yaml | 205 +++++++++ .../contracts/workflow-transition.yaml | 276 +++++++++++++ .../003-unified-workflow-engine/data-model.md | 388 ++++++++++++++++++ specs/003-unified-workflow-engine/plan.md | 272 ++++++++++++ .../003-unified-workflow-engine/quickstart.md | 205 +++++++++ specs/003-unified-workflow-engine/research.md | 209 ++++++++++ specs/003-unified-workflow-engine/spec.md | 216 ++++++++++ specs/003-unified-workflow-engine/tasks.md | 313 ++++++++++++++ ...9-add-version-no-to-workflow-instances.sql | 17 + ...ion-by-user-uuid-to-workflow-histories.sql | 14 + .../ADR-001-unified-workflow-engine.md | 184 ++++++++- 85 files changed, 6335 insertions(+), 100 deletions(-) create mode 100644 .agents/skills/diagnose/SKILL.md create mode 100644 .agents/skills/diagnose/scripts/hitl-loop.template.sh create mode 100644 .agents/skills/grill-with-docs/ADR-FORMAT.md create mode 100644 .agents/skills/grill-with-docs/CONTEXT-FORMAT.md create mode 100644 .agents/skills/grill-with-docs/SKILL.md create mode 100644 .agents/skills/setup-matt-pocock-skills/SKILL.md create mode 100644 .agents/skills/setup-matt-pocock-skills/domain.md create mode 100644 .agents/skills/setup-matt-pocock-skills/issue-tracker-github.md create mode 100644 .agents/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md create mode 100644 .agents/skills/setup-matt-pocock-skills/issue-tracker-local.md create mode 100644 .agents/skills/setup-matt-pocock-skills/triage-labels.md create mode 100644 .kilocode/skills/diagnose/SKILL.md create mode 100644 .kilocode/skills/diagnose/scripts/hitl-loop.template.sh create mode 100644 .kilocode/skills/grill-with-docs/ADR-FORMAT.md create mode 100644 .kilocode/skills/grill-with-docs/CONTEXT-FORMAT.md create mode 100644 .kilocode/skills/grill-with-docs/SKILL.md create mode 100644 .kilocode/skills/setup-matt-pocock-skills/SKILL.md create mode 100644 .kilocode/skills/setup-matt-pocock-skills/domain.md create mode 100644 .kilocode/skills/setup-matt-pocock-skills/issue-tracker-github.md create mode 100644 .kilocode/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md create mode 100644 .kilocode/skills/setup-matt-pocock-skills/issue-tracker-local.md create mode 100644 .kilocode/skills/setup-matt-pocock-skills/triage-labels.md create mode 100644 .qwen/skills/diagnose/SKILL.md create mode 100644 .qwen/skills/diagnose/scripts/hitl-loop.template.sh create mode 100644 .qwen/skills/grill-with-docs/ADR-FORMAT.md create mode 100644 .qwen/skills/grill-with-docs/CONTEXT-FORMAT.md create mode 100644 .qwen/skills/grill-with-docs/SKILL.md create mode 100644 .qwen/skills/setup-matt-pocock-skills/SKILL.md create mode 100644 .qwen/skills/setup-matt-pocock-skills/domain.md create mode 100644 .qwen/skills/setup-matt-pocock-skills/issue-tracker-github.md create mode 100644 .qwen/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md create mode 100644 .qwen/skills/setup-matt-pocock-skills/issue-tracker-local.md create mode 100644 .qwen/skills/setup-matt-pocock-skills/triage-labels.md create mode 100644 .windsurf/skills/diagnose/SKILL.md create mode 100644 .windsurf/skills/diagnose/scripts/hitl-loop.template.sh create mode 100644 .windsurf/skills/grill-with-docs/ADR-FORMAT.md create mode 100644 .windsurf/skills/grill-with-docs/CONTEXT-FORMAT.md create mode 100644 .windsurf/skills/grill-with-docs/SKILL.md create mode 100644 .windsurf/skills/setup-matt-pocock-skills/SKILL.md create mode 100644 .windsurf/skills/setup-matt-pocock-skills/domain.md create mode 100644 .windsurf/skills/setup-matt-pocock-skills/issue-tracker-github.md create mode 100644 .windsurf/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md create mode 100644 .windsurf/skills/setup-matt-pocock-skills/issue-tracker-local.md create mode 100644 .windsurf/skills/setup-matt-pocock-skills/triage-labels.md create mode 100644 .windsurf/workflows/setup-matt-pocock-skills.md create mode 100644 .windsurf/workflows/speckit-plan.md create mode 100644 backend/src/modules/workflow-engine/workflow-event.processor.spec.ts create mode 100644 backend/src/modules/workflow-engine/workflow-event.processor.ts create mode 100644 docs/agents/domain.md create mode 100644 docs/agents/issue-tracker.md create mode 100644 docs/agents/triage-labels.md create mode 100644 frontend/components/common/__tests__/file-preview-modal.test.tsx create mode 100644 frontend/components/workflows/__tests__/dsl-editor.test.tsx create mode 100644 specs/003-unified-workflow-engine/checklists/requirements.md create mode 100644 specs/003-unified-workflow-engine/contracts/workflow-definitions.yaml create mode 100644 specs/003-unified-workflow-engine/contracts/workflow-transition.yaml create mode 100644 specs/003-unified-workflow-engine/data-model.md create mode 100644 specs/003-unified-workflow-engine/plan.md create mode 100644 specs/003-unified-workflow-engine/quickstart.md create mode 100644 specs/003-unified-workflow-engine/research.md create mode 100644 specs/003-unified-workflow-engine/spec.md create mode 100644 specs/003-unified-workflow-engine/tasks.md create mode 100644 specs/03-Data-and-Storage/deltas/09-add-version-no-to-workflow-instances.sql create mode 100644 specs/03-Data-and-Storage/deltas/10-add-action-by-user-uuid-to-workflow-histories.sql diff --git a/.agents/skills/diagnose/SKILL.md b/.agents/skills/diagnose/SKILL.md new file mode 100644 index 0000000..ed55bda --- /dev/null +++ b/.agents/skills/diagnose/SKILL.md @@ -0,0 +1,117 @@ +--- +name: diagnose +description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing, or describes a performance regression. +--- + +# Diagnose + +A discipline for hard bugs. Skip phases only when explicitly justified. + +When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching. + +## Phase 1 — Build a feedback loop + +**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you. + +Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.** + +### Ways to construct one — try them in roughly this order + +1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e. +2. **Curl / HTTP script** against a running dev server. +3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot. +4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network. +5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation. +6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call. +7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode. +8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it. +9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs. +10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you. + +Build the right feedback loop, and the bug is 90% fixed. + +### Iterate on the loop itself + +Treat the loop as a product. Once you have _a_ loop, ask: + +- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.) +- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".) +- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.) + +A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower. + +### Non-deterministic bugs + +The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable. + +### When you genuinely cannot build a loop + +Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop. + +Do not proceed to Phase 2 until you have a loop you believe in. + +## Phase 2 — Reproduce + +Run the loop. Watch the bug appear. + +Confirm: + +- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix. +- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against). +- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it. + +Do not proceed until you reproduce the bug. + +## Phase 3 — Hypothesise + +Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea. + +Each hypothesis must be **falsifiable**: state the prediction it makes. + +> Format: "If is the cause, then will make the bug disappear / will make it worse." + +If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it. + +**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK. + +## Phase 4 — Instrument + +Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.** + +Tool preference: + +1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs. +2. **Targeted logs** at the boundaries that distinguish hypotheses. +3. Never "log everything and grep". + +**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die. + +**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second. + +## Phase 5 — Fix + regression test + +Write the regression test **before the fix** — but only if there is a **correct seam** for it. + +A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence. + +**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase. + +If a correct seam exists: + +1. Turn the minimised repro into a failing test at that seam. +2. Watch it fail. +3. Apply the fix. +4. Watch it pass. +5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario. + +## Phase 6 — Cleanup + post-mortem + +Required before declaring done: + +- [ ] Original repro no longer reproduces (re-run the Phase 1 loop) +- [ ] Regression test passes (or absence of seam is documented) +- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix) +- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location) +- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns + +**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started. diff --git a/.agents/skills/diagnose/scripts/hitl-loop.template.sh b/.agents/skills/diagnose/scripts/hitl-loop.template.sh new file mode 100644 index 0000000..40afc46 --- /dev/null +++ b/.agents/skills/diagnose/scripts/hitl-loop.template.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Human-in-the-loop reproduction loop. +# Copy this file, edit the steps below, and run it. +# The agent runs the script; the user follows prompts in their terminal. +# +# Usage: +# bash hitl-loop.template.sh +# +# Two helpers: +# step "" → show instruction, wait for Enter +# capture VAR "" → show question, read response into VAR +# +# At the end, captured values are printed as KEY=VALUE for the agent to parse. + +set -euo pipefail + +step() { + printf '\n>>> %s\n' "$1" + read -r -p " [Enter when done] " _ +} + +capture() { + local var="$1" question="$2" answer + printf '\n>>> %s\n' "$question" + read -r -p " > " answer + printf -v "$var" '%s' "$answer" +} + +# --- edit below --------------------------------------------------------- + +step "Open the app at http://localhost:3000 and sign in." + +capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)" + +capture ERROR_MSG "Paste the error message (or 'none'):" + +# --- edit above --------------------------------------------------------- + +printf '\n--- Captured ---\n' +printf 'ERRORED=%s\n' "$ERRORED" +printf 'ERROR_MSG=%s\n' "$ERROR_MSG" diff --git a/.agents/skills/grill-with-docs/ADR-FORMAT.md b/.agents/skills/grill-with-docs/ADR-FORMAT.md new file mode 100644 index 0000000..da7e78e --- /dev/null +++ b/.agents/skills/grill-with-docs/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md b/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md new file mode 100644 index 0000000..ddfa247 --- /dev/null +++ b/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md @@ -0,0 +1,77 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A concise description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account + +## Relationships + +- An **Order** produces one or more **Invoices** +- An **Invoice** belongs to exactly one **Customer** + +## Example dialogue + +> **Dev:** "When a **Customer** places an **Order**, do we create the **Invoice** immediately?" +> **Domain expert:** "No — an **Invoice** is only generated once a **Fulfillment** is confirmed." + +## Flagged ambiguities + +- "account" was used to mean both **Customer** and **User** — resolved: these are distinct concepts. +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid. +- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution. +- **Keep definitions tight.** One sentence max. Define what it IS, not what it does. +- **Show relationships.** Use bold term names and express cardinality where obvious. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. +- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/.agents/skills/grill-with-docs/SKILL.md b/.agents/skills/grill-with-docs/SKILL.md new file mode 100644 index 0000000..6dad6ad --- /dev/null +++ b/.agents/skills/grill-with-docs/SKILL.md @@ -0,0 +1,88 @@ +--- +name: grill-with-docs +description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions. +--- + + + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. + +If a question can be answered by exploring the codebase, explore the codebase instead. + + + + + +## Domain awareness + +During codebase exploration, also look for existing documentation: + +### File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +Don't couple `CONTEXT.md` to implementation details. Only include terms that are meaningful to domain experts. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). + + diff --git a/.agents/skills/setup-matt-pocock-skills/SKILL.md b/.agents/skills/setup-matt-pocock-skills/SKILL.md new file mode 100644 index 0000000..1ebc6e1 --- /dev/null +++ b/.agents/skills/setup-matt-pocock-skills/SKILL.md @@ -0,0 +1,121 @@ +--- +name: setup-matt-pocock-skills +description: Sets up an `## Agent skills` block in AGENTS.md/CLAUDE.md and `docs/agents/` so the engineering skills know this repo's issue tracker (GitHub or local markdown), triage label vocabulary, and domain doc layout. Run before first use of `to-issues`, `to-prd`, `triage`, `diagnose`, `tdd`, `improve-codebase-architecture`, or `zoom-out` — or if those skills appear to be missing context about the issue tracker, triage labels, or domain docs. +disable-model-invocation: true +--- + +# Setup Matt Pocock's Skills + +Scaffold the per-repo configuration that the engineering skills assume: + +- **Issue tracker** — where issues live (GitHub by default; local markdown is also supported out of the box) +- **Triage labels** — the strings used for the five canonical triage roles +- **Domain docs** — where `CONTEXT.md` and ADRs live, and the consumer rules for reading them + +This is a prompt-driven skill, not a deterministic script. Explore, present what you found, confirm with the user, then write. + +## Process + +### 1. Explore + +Look at the current repo to understand its starting state. Read whatever exists; don't assume: + +- `git remote -v` and `.git/config` — is this a GitHub repo? Which one? +- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an `## Agent skills` section in either? +- `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root +- `docs/adr/` and any `src/*/docs/adr/` directories +- `docs/agents/` — does this skill's prior output already exist? +- `.scratch/` — sign that a local-markdown issue tracker convention is already in use + +### 2. Present findings and ask + +Summarise what's present and what's missing. Then walk the user through the three decisions **one at a time** — present a section, get the user's answer, then move to the next. Don't dump all three at once. + +Assume the user does not know what these terms mean. Each section starts with a short explainer (what it is, why these skills need it, what changes if they pick differently). Then show the choices and the default. + +**Section A — Issue tracker.** + +> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-issues`, `triage`, `to-prd`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo. + +Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. Otherwise (or if the user prefers), offer: + +- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI) +- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI) +- **Local markdown** — issues live as files under `.scratch//` in this repo (good for solo projects or repos without a remote) +- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose + +**Section B — Triage label vocabulary.** + +> Explainer: When the `triage` skill processes an incoming issue, it moves it through a state machine — needs evaluation, waiting on reporter, ready for an AFK agent to pick up, ready for a human, or won't fix. To do that, it needs to apply labels (or the equivalent in your issue tracker) that match strings *you've actually configured*. If your repo already uses different label names (e.g. `bug:triage` instead of `needs-triage`), map them here so the skill applies the right ones instead of creating duplicates. + +The five canonical roles: + +- `needs-triage` — maintainer needs to evaluate +- `needs-info` — waiting on reporter +- `ready-for-agent` — fully specified, AFK-ready (an agent can pick it up with no human context) +- `ready-for-human` — needs human implementation +- `wontfix` — will not be actioned + +Default: each role's string equals its name. Ask the user if they want to override any. If their issue tracker has no existing labels, the defaults are fine. + +**Section C — Domain docs.** + +> Explainer: Some skills (`improve-codebase-architecture`, `diagnose`, `tdd`) read a `CONTEXT.md` file to learn the project's domain language, and `docs/adr/` for past architectural decisions. They need to know whether the repo has one global context or multiple (e.g. a monorepo with separate frontend/backend contexts) so they look in the right place. + +Confirm the layout: + +- **Single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. Most repos are this. +- **Multi-context** — `CONTEXT-MAP.md` at the root pointing to per-context `CONTEXT.md` files (typically a monorepo). + +### 3. Confirm and edit + +Show the user a draft of: + +- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules) +- The contents of `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md`, `docs/agents/domain.md` + +Let them edit before writing. + +### 4. Write + +**Pick the file to edit:** + +- If `CLAUDE.md` exists, edit it. +- Else if `AGENTS.md` exists, edit it. +- If neither exists, ask the user which one to create — don't pick for them. + +Never create `AGENTS.md` when `CLAUDE.md` already exists (or vice versa) — always edit the one that's already there. + +If an `## Agent skills` block already exists in the chosen file, update its contents in-place rather than appending a duplicate. Don't overwrite user edits to the surrounding sections. + +The block: + +```markdown +## Agent skills + +### Issue tracker + +[one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`. + +### Triage labels + +[one-line summary of the label vocabulary]. See `docs/agents/triage-labels.md`. + +### Domain docs + +[one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`. +``` + +Then write the three docs files using the seed templates in this skill folder as a starting point: + +- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker +- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker +- [issue-tracker-local.md](./issue-tracker-local.md) — local-markdown issue tracker +- [triage-labels.md](./triage-labels.md) — label mapping +- [domain.md](./domain.md) — domain doc consumer rules + layout + +For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description. + +### 5. Done + +Tell the user the setup is complete and which engineering skills will now read from these files. Mention they can edit `docs/agents/*.md` directly later — re-running this skill is only necessary if they want to switch issue trackers or restart from scratch. diff --git a/.agents/skills/setup-matt-pocock-skills/domain.md b/.agents/skills/setup-matt-pocock-skills/domain.md new file mode 100644 index 0000000..c97d6a6 --- /dev/null +++ b/.agents/skills/setup-matt-pocock-skills/domain.md @@ -0,0 +1,51 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root, or +- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. +- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-with-docs`) creates them lazily when terms or decisions actually get resolved. + +## File structure + +Single-context repo (most repos): + +``` +/ +├── CONTEXT.md +├── docs/adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +Multi-context repo (presence of `CONTEXT-MAP.md` at the root): + +``` +/ +├── CONTEXT-MAP.md +├── docs/adr/ ← system-wide decisions +└── src/ + ├── ordering/ + │ ├── CONTEXT.md + │ └── docs/adr/ ← context-specific decisions + └── billing/ + ├── CONTEXT.md + └── docs/adr/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-with-docs`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ diff --git a/.agents/skills/setup-matt-pocock-skills/issue-tracker-github.md b/.agents/skills/setup-matt-pocock-skills/issue-tracker-github.md new file mode 100644 index 0000000..cce77ec --- /dev/null +++ b/.agents/skills/setup-matt-pocock-skills/issue-tracker-github.md @@ -0,0 +1,22 @@ +# Issue tracker: GitHub + +Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations. + +## Conventions + +- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies. +- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --body "..."` +- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --comment "..."` + +Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone. + +## When a skill says "publish to the issue tracker" + +Create a GitHub issue. + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view --comments`. diff --git a/.agents/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md b/.agents/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md new file mode 100644 index 0000000..1993c58 --- /dev/null +++ b/.agents/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md @@ -0,0 +1,23 @@ +# Issue tracker: GitLab + +Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations. + +## Conventions + +- **Create an issue**: `glab issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions. Pass `--description -` to open an editor. +- **Read an issue**: `glab issue view --comments`. Use `-F json` for machine-readable output. +- **List issues**: `glab issue list --state opened -F json` with appropriate `--label` filters. Note that GitLab uses `opened` (not `open`) for the state value. +- **Comment on an issue**: `glab issue note --message "..."`. GitLab calls comments "notes". +- **Apply / remove labels**: `glab issue update --label "..."` / `--unlabel "..."`. Multiple labels can be comma-separated or by repeating the flag. +- **Close**: `glab issue close `. `glab issue close` does not accept a closing comment, so post the explanation first with `glab issue note --message "..."`, then close. +- **Merge requests**: GitLab calls PRs "merge requests". Use `glab mr create`, `glab mr view`, `glab mr note`, etc. — the same shape as `gh pr ...` with `mr` in place of `pr` and `note`/`--message` in place of `comment`/`--body`. + +Infer the repo from `git remote -v` — `glab` does this automatically when run inside a clone. + +## When a skill says "publish to the issue tracker" + +Create a GitLab issue. + +## When a skill says "fetch the relevant ticket" + +Run `glab issue view --comments`. diff --git a/.agents/skills/setup-matt-pocock-skills/issue-tracker-local.md b/.agents/skills/setup-matt-pocock-skills/issue-tracker-local.md new file mode 100644 index 0000000..a2f08fb --- /dev/null +++ b/.agents/skills/setup-matt-pocock-skills/issue-tracker-local.md @@ -0,0 +1,19 @@ +# Issue tracker: Local Markdown + +Issues and PRDs for this repo live as markdown files in `.scratch/`. + +## Conventions + +- One feature per directory: `.scratch//` +- The PRD is `.scratch//PRD.md` +- Implementation issues are `.scratch//issues/-.md`, numbered from `01` +- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings) +- Comments and conversation history append to the bottom of the file under a `## Comments` heading + +## When a skill says "publish to the issue tracker" + +Create a new file under `.scratch//` (creating the directory if needed). + +## When a skill says "fetch the relevant ticket" + +Read the file at the referenced path. The user will normally pass the path or the issue number directly. diff --git a/.agents/skills/setup-matt-pocock-skills/triage-labels.md b/.agents/skills/setup-matt-pocock-skills/triage-labels.md new file mode 100644 index 0000000..b716855 --- /dev/null +++ b/.agents/skills/setup-matt-pocock-skills/triage-labels.md @@ -0,0 +1,15 @@ +# Triage Labels + +The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. + +| Label in mattpocock/skills | Label in our tracker | Meaning | +| -------------------------- | -------------------- | ---------------------------------------- | +| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | +| `needs-info` | `needs-info` | Waiting on reporter for more information | +| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | +| `ready-for-human` | `ready-for-human` | Requires human implementation | +| `wontfix` | `wontfix` | Will not be actioned | + +When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. + +Edit the right-hand column to match whatever vocabulary you actually use. diff --git a/.kilocode/skills/diagnose/SKILL.md b/.kilocode/skills/diagnose/SKILL.md new file mode 100644 index 0000000..ed55bda --- /dev/null +++ b/.kilocode/skills/diagnose/SKILL.md @@ -0,0 +1,117 @@ +--- +name: diagnose +description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing, or describes a performance regression. +--- + +# Diagnose + +A discipline for hard bugs. Skip phases only when explicitly justified. + +When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching. + +## Phase 1 — Build a feedback loop + +**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you. + +Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.** + +### Ways to construct one — try them in roughly this order + +1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e. +2. **Curl / HTTP script** against a running dev server. +3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot. +4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network. +5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation. +6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call. +7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode. +8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it. +9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs. +10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you. + +Build the right feedback loop, and the bug is 90% fixed. + +### Iterate on the loop itself + +Treat the loop as a product. Once you have _a_ loop, ask: + +- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.) +- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".) +- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.) + +A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower. + +### Non-deterministic bugs + +The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable. + +### When you genuinely cannot build a loop + +Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop. + +Do not proceed to Phase 2 until you have a loop you believe in. + +## Phase 2 — Reproduce + +Run the loop. Watch the bug appear. + +Confirm: + +- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix. +- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against). +- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it. + +Do not proceed until you reproduce the bug. + +## Phase 3 — Hypothesise + +Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea. + +Each hypothesis must be **falsifiable**: state the prediction it makes. + +> Format: "If is the cause, then will make the bug disappear / will make it worse." + +If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it. + +**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK. + +## Phase 4 — Instrument + +Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.** + +Tool preference: + +1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs. +2. **Targeted logs** at the boundaries that distinguish hypotheses. +3. Never "log everything and grep". + +**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die. + +**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second. + +## Phase 5 — Fix + regression test + +Write the regression test **before the fix** — but only if there is a **correct seam** for it. + +A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence. + +**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase. + +If a correct seam exists: + +1. Turn the minimised repro into a failing test at that seam. +2. Watch it fail. +3. Apply the fix. +4. Watch it pass. +5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario. + +## Phase 6 — Cleanup + post-mortem + +Required before declaring done: + +- [ ] Original repro no longer reproduces (re-run the Phase 1 loop) +- [ ] Regression test passes (or absence of seam is documented) +- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix) +- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location) +- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns + +**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started. diff --git a/.kilocode/skills/diagnose/scripts/hitl-loop.template.sh b/.kilocode/skills/diagnose/scripts/hitl-loop.template.sh new file mode 100644 index 0000000..40afc46 --- /dev/null +++ b/.kilocode/skills/diagnose/scripts/hitl-loop.template.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Human-in-the-loop reproduction loop. +# Copy this file, edit the steps below, and run it. +# The agent runs the script; the user follows prompts in their terminal. +# +# Usage: +# bash hitl-loop.template.sh +# +# Two helpers: +# step "" → show instruction, wait for Enter +# capture VAR "" → show question, read response into VAR +# +# At the end, captured values are printed as KEY=VALUE for the agent to parse. + +set -euo pipefail + +step() { + printf '\n>>> %s\n' "$1" + read -r -p " [Enter when done] " _ +} + +capture() { + local var="$1" question="$2" answer + printf '\n>>> %s\n' "$question" + read -r -p " > " answer + printf -v "$var" '%s' "$answer" +} + +# --- edit below --------------------------------------------------------- + +step "Open the app at http://localhost:3000 and sign in." + +capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)" + +capture ERROR_MSG "Paste the error message (or 'none'):" + +# --- edit above --------------------------------------------------------- + +printf '\n--- Captured ---\n' +printf 'ERRORED=%s\n' "$ERRORED" +printf 'ERROR_MSG=%s\n' "$ERROR_MSG" diff --git a/.kilocode/skills/grill-with-docs/ADR-FORMAT.md b/.kilocode/skills/grill-with-docs/ADR-FORMAT.md new file mode 100644 index 0000000..da7e78e --- /dev/null +++ b/.kilocode/skills/grill-with-docs/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/.kilocode/skills/grill-with-docs/CONTEXT-FORMAT.md b/.kilocode/skills/grill-with-docs/CONTEXT-FORMAT.md new file mode 100644 index 0000000..ddfa247 --- /dev/null +++ b/.kilocode/skills/grill-with-docs/CONTEXT-FORMAT.md @@ -0,0 +1,77 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A concise description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account + +## Relationships + +- An **Order** produces one or more **Invoices** +- An **Invoice** belongs to exactly one **Customer** + +## Example dialogue + +> **Dev:** "When a **Customer** places an **Order**, do we create the **Invoice** immediately?" +> **Domain expert:** "No — an **Invoice** is only generated once a **Fulfillment** is confirmed." + +## Flagged ambiguities + +- "account" was used to mean both **Customer** and **User** — resolved: these are distinct concepts. +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid. +- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution. +- **Keep definitions tight.** One sentence max. Define what it IS, not what it does. +- **Show relationships.** Use bold term names and express cardinality where obvious. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. +- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/.kilocode/skills/grill-with-docs/SKILL.md b/.kilocode/skills/grill-with-docs/SKILL.md new file mode 100644 index 0000000..6dad6ad --- /dev/null +++ b/.kilocode/skills/grill-with-docs/SKILL.md @@ -0,0 +1,88 @@ +--- +name: grill-with-docs +description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions. +--- + + + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. + +If a question can be answered by exploring the codebase, explore the codebase instead. + + + + + +## Domain awareness + +During codebase exploration, also look for existing documentation: + +### File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +Don't couple `CONTEXT.md` to implementation details. Only include terms that are meaningful to domain experts. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). + + diff --git a/.kilocode/skills/setup-matt-pocock-skills/SKILL.md b/.kilocode/skills/setup-matt-pocock-skills/SKILL.md new file mode 100644 index 0000000..1ebc6e1 --- /dev/null +++ b/.kilocode/skills/setup-matt-pocock-skills/SKILL.md @@ -0,0 +1,121 @@ +--- +name: setup-matt-pocock-skills +description: Sets up an `## Agent skills` block in AGENTS.md/CLAUDE.md and `docs/agents/` so the engineering skills know this repo's issue tracker (GitHub or local markdown), triage label vocabulary, and domain doc layout. Run before first use of `to-issues`, `to-prd`, `triage`, `diagnose`, `tdd`, `improve-codebase-architecture`, or `zoom-out` — or if those skills appear to be missing context about the issue tracker, triage labels, or domain docs. +disable-model-invocation: true +--- + +# Setup Matt Pocock's Skills + +Scaffold the per-repo configuration that the engineering skills assume: + +- **Issue tracker** — where issues live (GitHub by default; local markdown is also supported out of the box) +- **Triage labels** — the strings used for the five canonical triage roles +- **Domain docs** — where `CONTEXT.md` and ADRs live, and the consumer rules for reading them + +This is a prompt-driven skill, not a deterministic script. Explore, present what you found, confirm with the user, then write. + +## Process + +### 1. Explore + +Look at the current repo to understand its starting state. Read whatever exists; don't assume: + +- `git remote -v` and `.git/config` — is this a GitHub repo? Which one? +- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an `## Agent skills` section in either? +- `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root +- `docs/adr/` and any `src/*/docs/adr/` directories +- `docs/agents/` — does this skill's prior output already exist? +- `.scratch/` — sign that a local-markdown issue tracker convention is already in use + +### 2. Present findings and ask + +Summarise what's present and what's missing. Then walk the user through the three decisions **one at a time** — present a section, get the user's answer, then move to the next. Don't dump all three at once. + +Assume the user does not know what these terms mean. Each section starts with a short explainer (what it is, why these skills need it, what changes if they pick differently). Then show the choices and the default. + +**Section A — Issue tracker.** + +> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-issues`, `triage`, `to-prd`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo. + +Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. Otherwise (or if the user prefers), offer: + +- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI) +- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI) +- **Local markdown** — issues live as files under `.scratch//` in this repo (good for solo projects or repos without a remote) +- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose + +**Section B — Triage label vocabulary.** + +> Explainer: When the `triage` skill processes an incoming issue, it moves it through a state machine — needs evaluation, waiting on reporter, ready for an AFK agent to pick up, ready for a human, or won't fix. To do that, it needs to apply labels (or the equivalent in your issue tracker) that match strings *you've actually configured*. If your repo already uses different label names (e.g. `bug:triage` instead of `needs-triage`), map them here so the skill applies the right ones instead of creating duplicates. + +The five canonical roles: + +- `needs-triage` — maintainer needs to evaluate +- `needs-info` — waiting on reporter +- `ready-for-agent` — fully specified, AFK-ready (an agent can pick it up with no human context) +- `ready-for-human` — needs human implementation +- `wontfix` — will not be actioned + +Default: each role's string equals its name. Ask the user if they want to override any. If their issue tracker has no existing labels, the defaults are fine. + +**Section C — Domain docs.** + +> Explainer: Some skills (`improve-codebase-architecture`, `diagnose`, `tdd`) read a `CONTEXT.md` file to learn the project's domain language, and `docs/adr/` for past architectural decisions. They need to know whether the repo has one global context or multiple (e.g. a monorepo with separate frontend/backend contexts) so they look in the right place. + +Confirm the layout: + +- **Single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. Most repos are this. +- **Multi-context** — `CONTEXT-MAP.md` at the root pointing to per-context `CONTEXT.md` files (typically a monorepo). + +### 3. Confirm and edit + +Show the user a draft of: + +- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules) +- The contents of `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md`, `docs/agents/domain.md` + +Let them edit before writing. + +### 4. Write + +**Pick the file to edit:** + +- If `CLAUDE.md` exists, edit it. +- Else if `AGENTS.md` exists, edit it. +- If neither exists, ask the user which one to create — don't pick for them. + +Never create `AGENTS.md` when `CLAUDE.md` already exists (or vice versa) — always edit the one that's already there. + +If an `## Agent skills` block already exists in the chosen file, update its contents in-place rather than appending a duplicate. Don't overwrite user edits to the surrounding sections. + +The block: + +```markdown +## Agent skills + +### Issue tracker + +[one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`. + +### Triage labels + +[one-line summary of the label vocabulary]. See `docs/agents/triage-labels.md`. + +### Domain docs + +[one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`. +``` + +Then write the three docs files using the seed templates in this skill folder as a starting point: + +- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker +- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker +- [issue-tracker-local.md](./issue-tracker-local.md) — local-markdown issue tracker +- [triage-labels.md](./triage-labels.md) — label mapping +- [domain.md](./domain.md) — domain doc consumer rules + layout + +For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description. + +### 5. Done + +Tell the user the setup is complete and which engineering skills will now read from these files. Mention they can edit `docs/agents/*.md` directly later — re-running this skill is only necessary if they want to switch issue trackers or restart from scratch. diff --git a/.kilocode/skills/setup-matt-pocock-skills/domain.md b/.kilocode/skills/setup-matt-pocock-skills/domain.md new file mode 100644 index 0000000..c97d6a6 --- /dev/null +++ b/.kilocode/skills/setup-matt-pocock-skills/domain.md @@ -0,0 +1,51 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root, or +- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. +- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-with-docs`) creates them lazily when terms or decisions actually get resolved. + +## File structure + +Single-context repo (most repos): + +``` +/ +├── CONTEXT.md +├── docs/adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +Multi-context repo (presence of `CONTEXT-MAP.md` at the root): + +``` +/ +├── CONTEXT-MAP.md +├── docs/adr/ ← system-wide decisions +└── src/ + ├── ordering/ + │ ├── CONTEXT.md + │ └── docs/adr/ ← context-specific decisions + └── billing/ + ├── CONTEXT.md + └── docs/adr/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-with-docs`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ diff --git a/.kilocode/skills/setup-matt-pocock-skills/issue-tracker-github.md b/.kilocode/skills/setup-matt-pocock-skills/issue-tracker-github.md new file mode 100644 index 0000000..cce77ec --- /dev/null +++ b/.kilocode/skills/setup-matt-pocock-skills/issue-tracker-github.md @@ -0,0 +1,22 @@ +# Issue tracker: GitHub + +Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations. + +## Conventions + +- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies. +- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --body "..."` +- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --comment "..."` + +Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone. + +## When a skill says "publish to the issue tracker" + +Create a GitHub issue. + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view --comments`. diff --git a/.kilocode/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md b/.kilocode/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md new file mode 100644 index 0000000..1993c58 --- /dev/null +++ b/.kilocode/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md @@ -0,0 +1,23 @@ +# Issue tracker: GitLab + +Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations. + +## Conventions + +- **Create an issue**: `glab issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions. Pass `--description -` to open an editor. +- **Read an issue**: `glab issue view --comments`. Use `-F json` for machine-readable output. +- **List issues**: `glab issue list --state opened -F json` with appropriate `--label` filters. Note that GitLab uses `opened` (not `open`) for the state value. +- **Comment on an issue**: `glab issue note --message "..."`. GitLab calls comments "notes". +- **Apply / remove labels**: `glab issue update --label "..."` / `--unlabel "..."`. Multiple labels can be comma-separated or by repeating the flag. +- **Close**: `glab issue close `. `glab issue close` does not accept a closing comment, so post the explanation first with `glab issue note --message "..."`, then close. +- **Merge requests**: GitLab calls PRs "merge requests". Use `glab mr create`, `glab mr view`, `glab mr note`, etc. — the same shape as `gh pr ...` with `mr` in place of `pr` and `note`/`--message` in place of `comment`/`--body`. + +Infer the repo from `git remote -v` — `glab` does this automatically when run inside a clone. + +## When a skill says "publish to the issue tracker" + +Create a GitLab issue. + +## When a skill says "fetch the relevant ticket" + +Run `glab issue view --comments`. diff --git a/.kilocode/skills/setup-matt-pocock-skills/issue-tracker-local.md b/.kilocode/skills/setup-matt-pocock-skills/issue-tracker-local.md new file mode 100644 index 0000000..a2f08fb --- /dev/null +++ b/.kilocode/skills/setup-matt-pocock-skills/issue-tracker-local.md @@ -0,0 +1,19 @@ +# Issue tracker: Local Markdown + +Issues and PRDs for this repo live as markdown files in `.scratch/`. + +## Conventions + +- One feature per directory: `.scratch//` +- The PRD is `.scratch//PRD.md` +- Implementation issues are `.scratch//issues/-.md`, numbered from `01` +- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings) +- Comments and conversation history append to the bottom of the file under a `## Comments` heading + +## When a skill says "publish to the issue tracker" + +Create a new file under `.scratch//` (creating the directory if needed). + +## When a skill says "fetch the relevant ticket" + +Read the file at the referenced path. The user will normally pass the path or the issue number directly. diff --git a/.kilocode/skills/setup-matt-pocock-skills/triage-labels.md b/.kilocode/skills/setup-matt-pocock-skills/triage-labels.md new file mode 100644 index 0000000..b716855 --- /dev/null +++ b/.kilocode/skills/setup-matt-pocock-skills/triage-labels.md @@ -0,0 +1,15 @@ +# Triage Labels + +The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. + +| Label in mattpocock/skills | Label in our tracker | Meaning | +| -------------------------- | -------------------- | ---------------------------------------- | +| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | +| `needs-info` | `needs-info` | Waiting on reporter for more information | +| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | +| `ready-for-human` | `ready-for-human` | Requires human implementation | +| `wontfix` | `wontfix` | Will not be actioned | + +When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. + +Edit the right-hand column to match whatever vocabulary you actually use. diff --git a/.qwen/skills/diagnose/SKILL.md b/.qwen/skills/diagnose/SKILL.md new file mode 100644 index 0000000..ed55bda --- /dev/null +++ b/.qwen/skills/diagnose/SKILL.md @@ -0,0 +1,117 @@ +--- +name: diagnose +description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing, or describes a performance regression. +--- + +# Diagnose + +A discipline for hard bugs. Skip phases only when explicitly justified. + +When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching. + +## Phase 1 — Build a feedback loop + +**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you. + +Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.** + +### Ways to construct one — try them in roughly this order + +1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e. +2. **Curl / HTTP script** against a running dev server. +3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot. +4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network. +5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation. +6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call. +7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode. +8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it. +9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs. +10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you. + +Build the right feedback loop, and the bug is 90% fixed. + +### Iterate on the loop itself + +Treat the loop as a product. Once you have _a_ loop, ask: + +- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.) +- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".) +- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.) + +A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower. + +### Non-deterministic bugs + +The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable. + +### When you genuinely cannot build a loop + +Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop. + +Do not proceed to Phase 2 until you have a loop you believe in. + +## Phase 2 — Reproduce + +Run the loop. Watch the bug appear. + +Confirm: + +- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix. +- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against). +- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it. + +Do not proceed until you reproduce the bug. + +## Phase 3 — Hypothesise + +Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea. + +Each hypothesis must be **falsifiable**: state the prediction it makes. + +> Format: "If is the cause, then will make the bug disappear / will make it worse." + +If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it. + +**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK. + +## Phase 4 — Instrument + +Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.** + +Tool preference: + +1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs. +2. **Targeted logs** at the boundaries that distinguish hypotheses. +3. Never "log everything and grep". + +**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die. + +**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second. + +## Phase 5 — Fix + regression test + +Write the regression test **before the fix** — but only if there is a **correct seam** for it. + +A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence. + +**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase. + +If a correct seam exists: + +1. Turn the minimised repro into a failing test at that seam. +2. Watch it fail. +3. Apply the fix. +4. Watch it pass. +5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario. + +## Phase 6 — Cleanup + post-mortem + +Required before declaring done: + +- [ ] Original repro no longer reproduces (re-run the Phase 1 loop) +- [ ] Regression test passes (or absence of seam is documented) +- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix) +- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location) +- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns + +**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started. diff --git a/.qwen/skills/diagnose/scripts/hitl-loop.template.sh b/.qwen/skills/diagnose/scripts/hitl-loop.template.sh new file mode 100644 index 0000000..40afc46 --- /dev/null +++ b/.qwen/skills/diagnose/scripts/hitl-loop.template.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Human-in-the-loop reproduction loop. +# Copy this file, edit the steps below, and run it. +# The agent runs the script; the user follows prompts in their terminal. +# +# Usage: +# bash hitl-loop.template.sh +# +# Two helpers: +# step "" → show instruction, wait for Enter +# capture VAR "" → show question, read response into VAR +# +# At the end, captured values are printed as KEY=VALUE for the agent to parse. + +set -euo pipefail + +step() { + printf '\n>>> %s\n' "$1" + read -r -p " [Enter when done] " _ +} + +capture() { + local var="$1" question="$2" answer + printf '\n>>> %s\n' "$question" + read -r -p " > " answer + printf -v "$var" '%s' "$answer" +} + +# --- edit below --------------------------------------------------------- + +step "Open the app at http://localhost:3000 and sign in." + +capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)" + +capture ERROR_MSG "Paste the error message (or 'none'):" + +# --- edit above --------------------------------------------------------- + +printf '\n--- Captured ---\n' +printf 'ERRORED=%s\n' "$ERRORED" +printf 'ERROR_MSG=%s\n' "$ERROR_MSG" diff --git a/.qwen/skills/grill-with-docs/ADR-FORMAT.md b/.qwen/skills/grill-with-docs/ADR-FORMAT.md new file mode 100644 index 0000000..da7e78e --- /dev/null +++ b/.qwen/skills/grill-with-docs/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/.qwen/skills/grill-with-docs/CONTEXT-FORMAT.md b/.qwen/skills/grill-with-docs/CONTEXT-FORMAT.md new file mode 100644 index 0000000..ddfa247 --- /dev/null +++ b/.qwen/skills/grill-with-docs/CONTEXT-FORMAT.md @@ -0,0 +1,77 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A concise description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account + +## Relationships + +- An **Order** produces one or more **Invoices** +- An **Invoice** belongs to exactly one **Customer** + +## Example dialogue + +> **Dev:** "When a **Customer** places an **Order**, do we create the **Invoice** immediately?" +> **Domain expert:** "No — an **Invoice** is only generated once a **Fulfillment** is confirmed." + +## Flagged ambiguities + +- "account" was used to mean both **Customer** and **User** — resolved: these are distinct concepts. +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid. +- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution. +- **Keep definitions tight.** One sentence max. Define what it IS, not what it does. +- **Show relationships.** Use bold term names and express cardinality where obvious. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. +- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/.qwen/skills/grill-with-docs/SKILL.md b/.qwen/skills/grill-with-docs/SKILL.md new file mode 100644 index 0000000..6dad6ad --- /dev/null +++ b/.qwen/skills/grill-with-docs/SKILL.md @@ -0,0 +1,88 @@ +--- +name: grill-with-docs +description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions. +--- + + + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. + +If a question can be answered by exploring the codebase, explore the codebase instead. + + + + + +## Domain awareness + +During codebase exploration, also look for existing documentation: + +### File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +Don't couple `CONTEXT.md` to implementation details. Only include terms that are meaningful to domain experts. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). + + diff --git a/.qwen/skills/setup-matt-pocock-skills/SKILL.md b/.qwen/skills/setup-matt-pocock-skills/SKILL.md new file mode 100644 index 0000000..1ebc6e1 --- /dev/null +++ b/.qwen/skills/setup-matt-pocock-skills/SKILL.md @@ -0,0 +1,121 @@ +--- +name: setup-matt-pocock-skills +description: Sets up an `## Agent skills` block in AGENTS.md/CLAUDE.md and `docs/agents/` so the engineering skills know this repo's issue tracker (GitHub or local markdown), triage label vocabulary, and domain doc layout. Run before first use of `to-issues`, `to-prd`, `triage`, `diagnose`, `tdd`, `improve-codebase-architecture`, or `zoom-out` — or if those skills appear to be missing context about the issue tracker, triage labels, or domain docs. +disable-model-invocation: true +--- + +# Setup Matt Pocock's Skills + +Scaffold the per-repo configuration that the engineering skills assume: + +- **Issue tracker** — where issues live (GitHub by default; local markdown is also supported out of the box) +- **Triage labels** — the strings used for the five canonical triage roles +- **Domain docs** — where `CONTEXT.md` and ADRs live, and the consumer rules for reading them + +This is a prompt-driven skill, not a deterministic script. Explore, present what you found, confirm with the user, then write. + +## Process + +### 1. Explore + +Look at the current repo to understand its starting state. Read whatever exists; don't assume: + +- `git remote -v` and `.git/config` — is this a GitHub repo? Which one? +- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an `## Agent skills` section in either? +- `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root +- `docs/adr/` and any `src/*/docs/adr/` directories +- `docs/agents/` — does this skill's prior output already exist? +- `.scratch/` — sign that a local-markdown issue tracker convention is already in use + +### 2. Present findings and ask + +Summarise what's present and what's missing. Then walk the user through the three decisions **one at a time** — present a section, get the user's answer, then move to the next. Don't dump all three at once. + +Assume the user does not know what these terms mean. Each section starts with a short explainer (what it is, why these skills need it, what changes if they pick differently). Then show the choices and the default. + +**Section A — Issue tracker.** + +> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-issues`, `triage`, `to-prd`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo. + +Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. Otherwise (or if the user prefers), offer: + +- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI) +- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI) +- **Local markdown** — issues live as files under `.scratch//` in this repo (good for solo projects or repos without a remote) +- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose + +**Section B — Triage label vocabulary.** + +> Explainer: When the `triage` skill processes an incoming issue, it moves it through a state machine — needs evaluation, waiting on reporter, ready for an AFK agent to pick up, ready for a human, or won't fix. To do that, it needs to apply labels (or the equivalent in your issue tracker) that match strings *you've actually configured*. If your repo already uses different label names (e.g. `bug:triage` instead of `needs-triage`), map them here so the skill applies the right ones instead of creating duplicates. + +The five canonical roles: + +- `needs-triage` — maintainer needs to evaluate +- `needs-info` — waiting on reporter +- `ready-for-agent` — fully specified, AFK-ready (an agent can pick it up with no human context) +- `ready-for-human` — needs human implementation +- `wontfix` — will not be actioned + +Default: each role's string equals its name. Ask the user if they want to override any. If their issue tracker has no existing labels, the defaults are fine. + +**Section C — Domain docs.** + +> Explainer: Some skills (`improve-codebase-architecture`, `diagnose`, `tdd`) read a `CONTEXT.md` file to learn the project's domain language, and `docs/adr/` for past architectural decisions. They need to know whether the repo has one global context or multiple (e.g. a monorepo with separate frontend/backend contexts) so they look in the right place. + +Confirm the layout: + +- **Single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. Most repos are this. +- **Multi-context** — `CONTEXT-MAP.md` at the root pointing to per-context `CONTEXT.md` files (typically a monorepo). + +### 3. Confirm and edit + +Show the user a draft of: + +- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules) +- The contents of `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md`, `docs/agents/domain.md` + +Let them edit before writing. + +### 4. Write + +**Pick the file to edit:** + +- If `CLAUDE.md` exists, edit it. +- Else if `AGENTS.md` exists, edit it. +- If neither exists, ask the user which one to create — don't pick for them. + +Never create `AGENTS.md` when `CLAUDE.md` already exists (or vice versa) — always edit the one that's already there. + +If an `## Agent skills` block already exists in the chosen file, update its contents in-place rather than appending a duplicate. Don't overwrite user edits to the surrounding sections. + +The block: + +```markdown +## Agent skills + +### Issue tracker + +[one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`. + +### Triage labels + +[one-line summary of the label vocabulary]. See `docs/agents/triage-labels.md`. + +### Domain docs + +[one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`. +``` + +Then write the three docs files using the seed templates in this skill folder as a starting point: + +- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker +- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker +- [issue-tracker-local.md](./issue-tracker-local.md) — local-markdown issue tracker +- [triage-labels.md](./triage-labels.md) — label mapping +- [domain.md](./domain.md) — domain doc consumer rules + layout + +For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description. + +### 5. Done + +Tell the user the setup is complete and which engineering skills will now read from these files. Mention they can edit `docs/agents/*.md` directly later — re-running this skill is only necessary if they want to switch issue trackers or restart from scratch. diff --git a/.qwen/skills/setup-matt-pocock-skills/domain.md b/.qwen/skills/setup-matt-pocock-skills/domain.md new file mode 100644 index 0000000..c97d6a6 --- /dev/null +++ b/.qwen/skills/setup-matt-pocock-skills/domain.md @@ -0,0 +1,51 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root, or +- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. +- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-with-docs`) creates them lazily when terms or decisions actually get resolved. + +## File structure + +Single-context repo (most repos): + +``` +/ +├── CONTEXT.md +├── docs/adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +Multi-context repo (presence of `CONTEXT-MAP.md` at the root): + +``` +/ +├── CONTEXT-MAP.md +├── docs/adr/ ← system-wide decisions +└── src/ + ├── ordering/ + │ ├── CONTEXT.md + │ └── docs/adr/ ← context-specific decisions + └── billing/ + ├── CONTEXT.md + └── docs/adr/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-with-docs`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ diff --git a/.qwen/skills/setup-matt-pocock-skills/issue-tracker-github.md b/.qwen/skills/setup-matt-pocock-skills/issue-tracker-github.md new file mode 100644 index 0000000..cce77ec --- /dev/null +++ b/.qwen/skills/setup-matt-pocock-skills/issue-tracker-github.md @@ -0,0 +1,22 @@ +# Issue tracker: GitHub + +Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations. + +## Conventions + +- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies. +- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --body "..."` +- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --comment "..."` + +Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone. + +## When a skill says "publish to the issue tracker" + +Create a GitHub issue. + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view --comments`. diff --git a/.qwen/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md b/.qwen/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md new file mode 100644 index 0000000..1993c58 --- /dev/null +++ b/.qwen/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md @@ -0,0 +1,23 @@ +# Issue tracker: GitLab + +Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations. + +## Conventions + +- **Create an issue**: `glab issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions. Pass `--description -` to open an editor. +- **Read an issue**: `glab issue view --comments`. Use `-F json` for machine-readable output. +- **List issues**: `glab issue list --state opened -F json` with appropriate `--label` filters. Note that GitLab uses `opened` (not `open`) for the state value. +- **Comment on an issue**: `glab issue note --message "..."`. GitLab calls comments "notes". +- **Apply / remove labels**: `glab issue update --label "..."` / `--unlabel "..."`. Multiple labels can be comma-separated or by repeating the flag. +- **Close**: `glab issue close `. `glab issue close` does not accept a closing comment, so post the explanation first with `glab issue note --message "..."`, then close. +- **Merge requests**: GitLab calls PRs "merge requests". Use `glab mr create`, `glab mr view`, `glab mr note`, etc. — the same shape as `gh pr ...` with `mr` in place of `pr` and `note`/`--message` in place of `comment`/`--body`. + +Infer the repo from `git remote -v` — `glab` does this automatically when run inside a clone. + +## When a skill says "publish to the issue tracker" + +Create a GitLab issue. + +## When a skill says "fetch the relevant ticket" + +Run `glab issue view --comments`. diff --git a/.qwen/skills/setup-matt-pocock-skills/issue-tracker-local.md b/.qwen/skills/setup-matt-pocock-skills/issue-tracker-local.md new file mode 100644 index 0000000..a2f08fb --- /dev/null +++ b/.qwen/skills/setup-matt-pocock-skills/issue-tracker-local.md @@ -0,0 +1,19 @@ +# Issue tracker: Local Markdown + +Issues and PRDs for this repo live as markdown files in `.scratch/`. + +## Conventions + +- One feature per directory: `.scratch//` +- The PRD is `.scratch//PRD.md` +- Implementation issues are `.scratch//issues/-.md`, numbered from `01` +- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings) +- Comments and conversation history append to the bottom of the file under a `## Comments` heading + +## When a skill says "publish to the issue tracker" + +Create a new file under `.scratch//` (creating the directory if needed). + +## When a skill says "fetch the relevant ticket" + +Read the file at the referenced path. The user will normally pass the path or the issue number directly. diff --git a/.qwen/skills/setup-matt-pocock-skills/triage-labels.md b/.qwen/skills/setup-matt-pocock-skills/triage-labels.md new file mode 100644 index 0000000..b716855 --- /dev/null +++ b/.qwen/skills/setup-matt-pocock-skills/triage-labels.md @@ -0,0 +1,15 @@ +# Triage Labels + +The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. + +| Label in mattpocock/skills | Label in our tracker | Meaning | +| -------------------------- | -------------------- | ---------------------------------------- | +| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | +| `needs-info` | `needs-info` | Waiting on reporter for more information | +| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | +| `ready-for-human` | `ready-for-human` | Requires human implementation | +| `wontfix` | `wontfix` | Will not be actioned | + +When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. + +Edit the right-hand column to match whatever vocabulary you actually use. diff --git a/.windsurf/skills/diagnose/SKILL.md b/.windsurf/skills/diagnose/SKILL.md new file mode 100644 index 0000000..ed55bda --- /dev/null +++ b/.windsurf/skills/diagnose/SKILL.md @@ -0,0 +1,117 @@ +--- +name: diagnose +description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing, or describes a performance regression. +--- + +# Diagnose + +A discipline for hard bugs. Skip phases only when explicitly justified. + +When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching. + +## Phase 1 — Build a feedback loop + +**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you. + +Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.** + +### Ways to construct one — try them in roughly this order + +1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e. +2. **Curl / HTTP script** against a running dev server. +3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot. +4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network. +5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation. +6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call. +7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode. +8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it. +9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs. +10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you. + +Build the right feedback loop, and the bug is 90% fixed. + +### Iterate on the loop itself + +Treat the loop as a product. Once you have _a_ loop, ask: + +- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.) +- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".) +- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.) + +A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower. + +### Non-deterministic bugs + +The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable. + +### When you genuinely cannot build a loop + +Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop. + +Do not proceed to Phase 2 until you have a loop you believe in. + +## Phase 2 — Reproduce + +Run the loop. Watch the bug appear. + +Confirm: + +- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix. +- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against). +- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it. + +Do not proceed until you reproduce the bug. + +## Phase 3 — Hypothesise + +Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea. + +Each hypothesis must be **falsifiable**: state the prediction it makes. + +> Format: "If is the cause, then will make the bug disappear / will make it worse." + +If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it. + +**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK. + +## Phase 4 — Instrument + +Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.** + +Tool preference: + +1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs. +2. **Targeted logs** at the boundaries that distinguish hypotheses. +3. Never "log everything and grep". + +**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die. + +**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second. + +## Phase 5 — Fix + regression test + +Write the regression test **before the fix** — but only if there is a **correct seam** for it. + +A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence. + +**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase. + +If a correct seam exists: + +1. Turn the minimised repro into a failing test at that seam. +2. Watch it fail. +3. Apply the fix. +4. Watch it pass. +5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario. + +## Phase 6 — Cleanup + post-mortem + +Required before declaring done: + +- [ ] Original repro no longer reproduces (re-run the Phase 1 loop) +- [ ] Regression test passes (or absence of seam is documented) +- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix) +- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location) +- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns + +**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started. diff --git a/.windsurf/skills/diagnose/scripts/hitl-loop.template.sh b/.windsurf/skills/diagnose/scripts/hitl-loop.template.sh new file mode 100644 index 0000000..40afc46 --- /dev/null +++ b/.windsurf/skills/diagnose/scripts/hitl-loop.template.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Human-in-the-loop reproduction loop. +# Copy this file, edit the steps below, and run it. +# The agent runs the script; the user follows prompts in their terminal. +# +# Usage: +# bash hitl-loop.template.sh +# +# Two helpers: +# step "" → show instruction, wait for Enter +# capture VAR "" → show question, read response into VAR +# +# At the end, captured values are printed as KEY=VALUE for the agent to parse. + +set -euo pipefail + +step() { + printf '\n>>> %s\n' "$1" + read -r -p " [Enter when done] " _ +} + +capture() { + local var="$1" question="$2" answer + printf '\n>>> %s\n' "$question" + read -r -p " > " answer + printf -v "$var" '%s' "$answer" +} + +# --- edit below --------------------------------------------------------- + +step "Open the app at http://localhost:3000 and sign in." + +capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)" + +capture ERROR_MSG "Paste the error message (or 'none'):" + +# --- edit above --------------------------------------------------------- + +printf '\n--- Captured ---\n' +printf 'ERRORED=%s\n' "$ERRORED" +printf 'ERROR_MSG=%s\n' "$ERROR_MSG" diff --git a/.windsurf/skills/grill-with-docs/ADR-FORMAT.md b/.windsurf/skills/grill-with-docs/ADR-FORMAT.md new file mode 100644 index 0000000..da7e78e --- /dev/null +++ b/.windsurf/skills/grill-with-docs/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/.windsurf/skills/grill-with-docs/CONTEXT-FORMAT.md b/.windsurf/skills/grill-with-docs/CONTEXT-FORMAT.md new file mode 100644 index 0000000..ddfa247 --- /dev/null +++ b/.windsurf/skills/grill-with-docs/CONTEXT-FORMAT.md @@ -0,0 +1,77 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A concise description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account + +## Relationships + +- An **Order** produces one or more **Invoices** +- An **Invoice** belongs to exactly one **Customer** + +## Example dialogue + +> **Dev:** "When a **Customer** places an **Order**, do we create the **Invoice** immediately?" +> **Domain expert:** "No — an **Invoice** is only generated once a **Fulfillment** is confirmed." + +## Flagged ambiguities + +- "account" was used to mean both **Customer** and **User** — resolved: these are distinct concepts. +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid. +- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution. +- **Keep definitions tight.** One sentence max. Define what it IS, not what it does. +- **Show relationships.** Use bold term names and express cardinality where obvious. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. +- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/.windsurf/skills/grill-with-docs/SKILL.md b/.windsurf/skills/grill-with-docs/SKILL.md new file mode 100644 index 0000000..6dad6ad --- /dev/null +++ b/.windsurf/skills/grill-with-docs/SKILL.md @@ -0,0 +1,88 @@ +--- +name: grill-with-docs +description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions. +--- + + + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. + +If a question can be answered by exploring the codebase, explore the codebase instead. + + + + + +## Domain awareness + +During codebase exploration, also look for existing documentation: + +### File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +Don't couple `CONTEXT.md` to implementation details. Only include terms that are meaningful to domain experts. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). + + diff --git a/.windsurf/skills/setup-matt-pocock-skills/SKILL.md b/.windsurf/skills/setup-matt-pocock-skills/SKILL.md new file mode 100644 index 0000000..1ebc6e1 --- /dev/null +++ b/.windsurf/skills/setup-matt-pocock-skills/SKILL.md @@ -0,0 +1,121 @@ +--- +name: setup-matt-pocock-skills +description: Sets up an `## Agent skills` block in AGENTS.md/CLAUDE.md and `docs/agents/` so the engineering skills know this repo's issue tracker (GitHub or local markdown), triage label vocabulary, and domain doc layout. Run before first use of `to-issues`, `to-prd`, `triage`, `diagnose`, `tdd`, `improve-codebase-architecture`, or `zoom-out` — or if those skills appear to be missing context about the issue tracker, triage labels, or domain docs. +disable-model-invocation: true +--- + +# Setup Matt Pocock's Skills + +Scaffold the per-repo configuration that the engineering skills assume: + +- **Issue tracker** — where issues live (GitHub by default; local markdown is also supported out of the box) +- **Triage labels** — the strings used for the five canonical triage roles +- **Domain docs** — where `CONTEXT.md` and ADRs live, and the consumer rules for reading them + +This is a prompt-driven skill, not a deterministic script. Explore, present what you found, confirm with the user, then write. + +## Process + +### 1. Explore + +Look at the current repo to understand its starting state. Read whatever exists; don't assume: + +- `git remote -v` and `.git/config` — is this a GitHub repo? Which one? +- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an `## Agent skills` section in either? +- `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root +- `docs/adr/` and any `src/*/docs/adr/` directories +- `docs/agents/` — does this skill's prior output already exist? +- `.scratch/` — sign that a local-markdown issue tracker convention is already in use + +### 2. Present findings and ask + +Summarise what's present and what's missing. Then walk the user through the three decisions **one at a time** — present a section, get the user's answer, then move to the next. Don't dump all three at once. + +Assume the user does not know what these terms mean. Each section starts with a short explainer (what it is, why these skills need it, what changes if they pick differently). Then show the choices and the default. + +**Section A — Issue tracker.** + +> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-issues`, `triage`, `to-prd`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo. + +Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. Otherwise (or if the user prefers), offer: + +- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI) +- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI) +- **Local markdown** — issues live as files under `.scratch//` in this repo (good for solo projects or repos without a remote) +- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose + +**Section B — Triage label vocabulary.** + +> Explainer: When the `triage` skill processes an incoming issue, it moves it through a state machine — needs evaluation, waiting on reporter, ready for an AFK agent to pick up, ready for a human, or won't fix. To do that, it needs to apply labels (or the equivalent in your issue tracker) that match strings *you've actually configured*. If your repo already uses different label names (e.g. `bug:triage` instead of `needs-triage`), map them here so the skill applies the right ones instead of creating duplicates. + +The five canonical roles: + +- `needs-triage` — maintainer needs to evaluate +- `needs-info` — waiting on reporter +- `ready-for-agent` — fully specified, AFK-ready (an agent can pick it up with no human context) +- `ready-for-human` — needs human implementation +- `wontfix` — will not be actioned + +Default: each role's string equals its name. Ask the user if they want to override any. If their issue tracker has no existing labels, the defaults are fine. + +**Section C — Domain docs.** + +> Explainer: Some skills (`improve-codebase-architecture`, `diagnose`, `tdd`) read a `CONTEXT.md` file to learn the project's domain language, and `docs/adr/` for past architectural decisions. They need to know whether the repo has one global context or multiple (e.g. a monorepo with separate frontend/backend contexts) so they look in the right place. + +Confirm the layout: + +- **Single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. Most repos are this. +- **Multi-context** — `CONTEXT-MAP.md` at the root pointing to per-context `CONTEXT.md` files (typically a monorepo). + +### 3. Confirm and edit + +Show the user a draft of: + +- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules) +- The contents of `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md`, `docs/agents/domain.md` + +Let them edit before writing. + +### 4. Write + +**Pick the file to edit:** + +- If `CLAUDE.md` exists, edit it. +- Else if `AGENTS.md` exists, edit it. +- If neither exists, ask the user which one to create — don't pick for them. + +Never create `AGENTS.md` when `CLAUDE.md` already exists (or vice versa) — always edit the one that's already there. + +If an `## Agent skills` block already exists in the chosen file, update its contents in-place rather than appending a duplicate. Don't overwrite user edits to the surrounding sections. + +The block: + +```markdown +## Agent skills + +### Issue tracker + +[one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`. + +### Triage labels + +[one-line summary of the label vocabulary]. See `docs/agents/triage-labels.md`. + +### Domain docs + +[one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`. +``` + +Then write the three docs files using the seed templates in this skill folder as a starting point: + +- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker +- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker +- [issue-tracker-local.md](./issue-tracker-local.md) — local-markdown issue tracker +- [triage-labels.md](./triage-labels.md) — label mapping +- [domain.md](./domain.md) — domain doc consumer rules + layout + +For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description. + +### 5. Done + +Tell the user the setup is complete and which engineering skills will now read from these files. Mention they can edit `docs/agents/*.md` directly later — re-running this skill is only necessary if they want to switch issue trackers or restart from scratch. diff --git a/.windsurf/skills/setup-matt-pocock-skills/domain.md b/.windsurf/skills/setup-matt-pocock-skills/domain.md new file mode 100644 index 0000000..c97d6a6 --- /dev/null +++ b/.windsurf/skills/setup-matt-pocock-skills/domain.md @@ -0,0 +1,51 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root, or +- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. +- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-with-docs`) creates them lazily when terms or decisions actually get resolved. + +## File structure + +Single-context repo (most repos): + +``` +/ +├── CONTEXT.md +├── docs/adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +Multi-context repo (presence of `CONTEXT-MAP.md` at the root): + +``` +/ +├── CONTEXT-MAP.md +├── docs/adr/ ← system-wide decisions +└── src/ + ├── ordering/ + │ ├── CONTEXT.md + │ └── docs/adr/ ← context-specific decisions + └── billing/ + ├── CONTEXT.md + └── docs/adr/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-with-docs`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ diff --git a/.windsurf/skills/setup-matt-pocock-skills/issue-tracker-github.md b/.windsurf/skills/setup-matt-pocock-skills/issue-tracker-github.md new file mode 100644 index 0000000..cce77ec --- /dev/null +++ b/.windsurf/skills/setup-matt-pocock-skills/issue-tracker-github.md @@ -0,0 +1,22 @@ +# Issue tracker: GitHub + +Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations. + +## Conventions + +- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies. +- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --body "..."` +- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --comment "..."` + +Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone. + +## When a skill says "publish to the issue tracker" + +Create a GitHub issue. + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view --comments`. diff --git a/.windsurf/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md b/.windsurf/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md new file mode 100644 index 0000000..1993c58 --- /dev/null +++ b/.windsurf/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md @@ -0,0 +1,23 @@ +# Issue tracker: GitLab + +Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations. + +## Conventions + +- **Create an issue**: `glab issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions. Pass `--description -` to open an editor. +- **Read an issue**: `glab issue view --comments`. Use `-F json` for machine-readable output. +- **List issues**: `glab issue list --state opened -F json` with appropriate `--label` filters. Note that GitLab uses `opened` (not `open`) for the state value. +- **Comment on an issue**: `glab issue note --message "..."`. GitLab calls comments "notes". +- **Apply / remove labels**: `glab issue update --label "..."` / `--unlabel "..."`. Multiple labels can be comma-separated or by repeating the flag. +- **Close**: `glab issue close `. `glab issue close` does not accept a closing comment, so post the explanation first with `glab issue note --message "..."`, then close. +- **Merge requests**: GitLab calls PRs "merge requests". Use `glab mr create`, `glab mr view`, `glab mr note`, etc. — the same shape as `gh pr ...` with `mr` in place of `pr` and `note`/`--message` in place of `comment`/`--body`. + +Infer the repo from `git remote -v` — `glab` does this automatically when run inside a clone. + +## When a skill says "publish to the issue tracker" + +Create a GitLab issue. + +## When a skill says "fetch the relevant ticket" + +Run `glab issue view --comments`. diff --git a/.windsurf/skills/setup-matt-pocock-skills/issue-tracker-local.md b/.windsurf/skills/setup-matt-pocock-skills/issue-tracker-local.md new file mode 100644 index 0000000..a2f08fb --- /dev/null +++ b/.windsurf/skills/setup-matt-pocock-skills/issue-tracker-local.md @@ -0,0 +1,19 @@ +# Issue tracker: Local Markdown + +Issues and PRDs for this repo live as markdown files in `.scratch/`. + +## Conventions + +- One feature per directory: `.scratch//` +- The PRD is `.scratch//PRD.md` +- Implementation issues are `.scratch//issues/-.md`, numbered from `01` +- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings) +- Comments and conversation history append to the bottom of the file under a `## Comments` heading + +## When a skill says "publish to the issue tracker" + +Create a new file under `.scratch//` (creating the directory if needed). + +## When a skill says "fetch the relevant ticket" + +Read the file at the referenced path. The user will normally pass the path or the issue number directly. diff --git a/.windsurf/skills/setup-matt-pocock-skills/triage-labels.md b/.windsurf/skills/setup-matt-pocock-skills/triage-labels.md new file mode 100644 index 0000000..b716855 --- /dev/null +++ b/.windsurf/skills/setup-matt-pocock-skills/triage-labels.md @@ -0,0 +1,15 @@ +# Triage Labels + +The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. + +| Label in mattpocock/skills | Label in our tracker | Meaning | +| -------------------------- | -------------------- | ---------------------------------------- | +| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | +| `needs-info` | `needs-info` | Waiting on reporter for more information | +| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | +| `ready-for-human` | `ready-for-human` | Requires human implementation | +| `wontfix` | `wontfix` | Will not be actioned | + +When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. + +Edit the right-hand column to match whatever vocabulary you actually use. diff --git a/.windsurf/workflows/setup-matt-pocock-skills.md b/.windsurf/workflows/setup-matt-pocock-skills.md new file mode 100644 index 0000000..e69de29 diff --git a/.windsurf/workflows/speckit-plan.md b/.windsurf/workflows/speckit-plan.md new file mode 100644 index 0000000..e69de29 diff --git a/AGENTS.md b/AGENTS.md index 2f21ca0..9572627 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -375,6 +375,22 @@ When user asks about... check these files: --- +## Agent skills + +### Issue tracker + +Issues live in the self-hosted Gitea repo at git.np-dms.work:2222. See `docs/agents/issue-tracker.md`. + +### Triage labels + +Default label vocabulary (no custom mapping). See `docs/agents/triage-labels.md`. + +### Domain docs + +Single-context repo with domain documentation in `specs/`. See `docs/agents/domain.md`. + +--- + ## 📚 Full Documentation This file is a **quick reference**. For detailed information: diff --git a/backend/src/modules/correspondence/correspondence.service.ts b/backend/src/modules/correspondence/correspondence.service.ts index 4e9723d..d804bd2 100644 --- a/backend/src/modules/correspondence/correspondence.service.ts +++ b/backend/src/modules/correspondence/correspondence.service.ts @@ -551,7 +551,19 @@ export class CorrespondenceService { if (!correspondence) { throw new NotFoundException('Correspondence', publicId); } - return correspondence; + + // ADR-021: expose live workflow state (null-safe — Draft \u0e22\u0e31\u0e07\u0e44\u0e21\u0e48\u0e21\u0e35 workflow instance) + const workflowInstance = await this.workflowEngine.getInstanceByEntity( + 'correspondence', + correspondence.publicId + ); + + return { + ...correspondence, + workflowInstanceId: workflowInstance?.id ?? null, + workflowState: workflowInstance?.currentState ?? null, + availableActions: workflowInstance?.availableActions ?? [], + }; } async addReference(id: number, dto: AddReferenceDto) { diff --git a/backend/src/modules/workflow-engine/dto/workflow-history-item.dto.ts b/backend/src/modules/workflow-engine/dto/workflow-history-item.dto.ts index f5fe847..be80f91 100644 --- a/backend/src/modules/workflow-engine/dto/workflow-history-item.dto.ts +++ b/backend/src/modules/workflow-engine/dto/workflow-history-item.dto.ts @@ -12,6 +12,8 @@ export class WorkflowHistoryItemDto { toState!: string; action!: string; actionByUserId?: number; + // ADR-019: UUID ของ User ผู้ดำเนินการ — expose แทน INT PK ในทุก API Response + actorUuid?: string; comment?: string; metadata?: Record; attachments!: AttachmentSummaryDto[]; diff --git a/backend/src/modules/workflow-engine/dto/workflow-transition.dto.ts b/backend/src/modules/workflow-engine/dto/workflow-transition.dto.ts index 9e25c51..37d8059 100644 --- a/backend/src/modules/workflow-engine/dto/workflow-transition.dto.ts +++ b/backend/src/modules/workflow-engine/dto/workflow-transition.dto.ts @@ -4,11 +4,13 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ArrayMaxSize, IsArray, + IsInt, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, + Min, } from 'class-validator'; export class WorkflowTransitionDto { @@ -47,4 +49,15 @@ export class WorkflowTransitionDto { @ArrayMaxSize(20) @IsOptional() attachmentPublicIds?: string[]; + + @ApiPropertyOptional({ + description: + 'Optimistic lock version — ส่งค่าที่ได้จาก GET /instances/:id เพื่อป้องกัน Double-approval (ADR-001 v1.1 FR-002). Server ตอบ 409 ถ้าค่าไม่ตรง', + example: 5, + minimum: 1, + }) + @IsInt() + @Min(1) + @IsOptional() + versionNo?: number; } diff --git a/backend/src/modules/workflow-engine/entities/workflow-history.entity.ts b/backend/src/modules/workflow-engine/entities/workflow-history.entity.ts index d485396..4c29177 100644 --- a/backend/src/modules/workflow-engine/entities/workflow-history.entity.ts +++ b/backend/src/modules/workflow-engine/entities/workflow-history.entity.ts @@ -47,6 +47,17 @@ export class WorkflowHistory { }) actionByUserId?: number; + // ADR-019: UUID ของ User ผู้ดำเนินการ — expose ใน API Response แทน INT PK + // NULL = System Action หรือ Pre-migration record (Delta 10) + @Column({ + name: 'action_by_user_uuid', + length: 36, + nullable: true, + comment: + 'UUID ของ User ผู้ดำเนินการ — ใช้ใน API Response per ADR-019. INT FK action_by_user_id ยังคงอยู่สำหรับ Internal use', + }) + actionByUserUuid?: string; + @Column({ type: 'text', nullable: true, comment: 'ความเห็นประกอบการอนุมัติ' }) comment?: string; diff --git a/backend/src/modules/workflow-engine/entities/workflow-instance.entity.ts b/backend/src/modules/workflow-engine/entities/workflow-instance.entity.ts index 762b2cb..d1d0338 100644 --- a/backend/src/modules/workflow-engine/entities/workflow-instance.entity.ts +++ b/backend/src/modules/workflow-engine/entities/workflow-instance.entity.ts @@ -85,4 +85,15 @@ export class WorkflowInstance { @UpdateDateColumn({ name: 'updated_at' }) updatedAt!: Date; + + // ADR-001 v1.1 FR-002: Optimistic lock — incremented on every successful transition + // Client ส่งค่านี้มาด้วยทุกครั้งที่ transition; Server reject HTTP 409 ถ้าไม่ตรง + @Column({ + name: 'version_no', + type: 'int', + default: 1, + comment: + 'Optimistic lock counter — incremented on each successful transition (ADR-001 v1.1 FR-002)', + }) + versionNo!: number; } diff --git a/backend/src/modules/workflow-engine/guards/workflow-transition.guard.spec.ts b/backend/src/modules/workflow-engine/guards/workflow-transition.guard.spec.ts index 1731211..8895a7e 100644 --- a/backend/src/modules/workflow-engine/guards/workflow-transition.guard.spec.ts +++ b/backend/src/modules/workflow-engine/guards/workflow-transition.guard.spec.ts @@ -34,9 +34,11 @@ describe('WorkflowTransitionGuard', () => { const mockRequest = ( params: Record = {}, - user: MockUserPayload = mockUser + user: MockUserPayload = mockUser, + action = 'APPROVE' ): Partial => ({ params, + body: { action }, user: user as RequestWithUser['user'], }); @@ -120,6 +122,7 @@ describe('WorkflowTransitionGuard', () => { expect(userService.getUserPermissions).toHaveBeenCalledWith(123); expect(instanceRepo.findOne).toHaveBeenCalledWith({ where: { id: 'instance-123' }, + relations: ['definition'], }); }); @@ -276,6 +279,130 @@ describe('WorkflowTransitionGuard', () => { }); }); + // T025: DSL require.role → CASL ability mapping tests + describe('DSL CASL Role Mapping (FR-002a)', () => { + it('should allow access when DSL requires OrgAdmin role and user has organization.manage_users', async () => { + userService.getUserPermissions.mockResolvedValue([ + 'organization.manage_users', + ]); + const mockInstance = { + id: 'instance-dsl-1', + currentState: 'PENDING_REVIEW', + context: { organizationId: 99 }, // Different org — Level 2 would deny + contractId: null, + definition: { + compiled: { + states: { + PENDING_REVIEW: { + transitions: { + APPROVE: { requirements: { roles: ['OrgAdmin'] } }, + }, + }, + }, + }, + }, + }; + instanceRepo.findOne.mockResolvedValue(mockInstance); + const context = mockContext( + mockRequest({ id: 'instance-dsl-1' }, mockUser, 'APPROVE') + ); + + const result = await guard.canActivate(context); + + expect(result).toBe(true); + }); + + it('should allow access when DSL requires ContractMember and user has contract.view', async () => { + userService.getUserPermissions.mockResolvedValue(['contract.view']); + const mockInstance = { + id: 'instance-dsl-2', + currentState: 'REVIEW', + context: { organizationId: 99 }, + contractId: null, + definition: { + compiled: { + states: { + REVIEW: { + transitions: { + SUBMIT: { requirements: { roles: ['ContractMember'] } }, + }, + }, + }, + }, + }, + }; + instanceRepo.findOne.mockResolvedValue(mockInstance); + const context = mockContext( + mockRequest({ id: 'instance-dsl-2' }, mockUser, 'SUBMIT') + ); + + const result = await guard.canActivate(context); + + expect(result).toBe(true); + }); + + it('should deny when DSL requires OrgAdmin but user only has contract.view', async () => { + userService.getUserPermissions.mockResolvedValue(['contract.view']); + const mockInstance = { + id: 'instance-dsl-3', + currentState: 'PENDING', + context: { organizationId: 99 }, + contractId: null, + definition: { + compiled: { + states: { + PENDING: { + transitions: { + APPROVE: { requirements: { roles: ['OrgAdmin'] } }, + }, + }, + }, + }, + }, + }; + instanceRepo.findOne.mockResolvedValue(mockInstance); + const context = mockContext( + mockRequest({ id: 'instance-dsl-3' }, mockUser, 'APPROVE') + ); + + await expect(guard.canActivate(context)).rejects.toThrow( + ForbiddenException + ); + }); + + it('should fall through to Level 3 when DSL role is AssignedHandler', async () => { + userService.getUserPermissions.mockResolvedValue(['document.view']); + const mockInstance = { + id: 'instance-dsl-4', + currentState: 'ASSIGNED', + context: { organizationId: 99, assignedUserId: 123 }, // same as mockUser.user_id + contractId: null, + definition: { + compiled: { + states: { + ASSIGNED: { + transitions: { + COMPLETE: { + requirements: { roles: ['AssignedHandler'] }, + }, + }, + }, + }, + }, + }, + }; + instanceRepo.findOne.mockResolvedValue(mockInstance); + const context = mockContext( + mockRequest({ id: 'instance-dsl-4' }, mockUser, 'COMPLETE') + ); + + // AssignedHandler → falls to Level 3 check → passes because assignedUserId === user_id + const result = await guard.canActivate(context); + + expect(result).toBe(true); + }); + }); + describe('Level 4: Unauthorized Users', () => { it('should deny access for regular users without any special permissions', async () => { // Arrange diff --git a/backend/src/modules/workflow-engine/guards/workflow-transition.guard.ts b/backend/src/modules/workflow-engine/guards/workflow-transition.guard.ts index 391b7b7..a7c744c 100644 --- a/backend/src/modules/workflow-engine/guards/workflow-transition.guard.ts +++ b/backend/src/modules/workflow-engine/guards/workflow-transition.guard.ts @@ -12,7 +12,17 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { WorkflowInstance } from '../entities/workflow-instance.entity'; +import { CompiledWorkflow } from '../workflow-dsl.service'; import { UserService } from '../../../modules/user/user.service'; + +// FR-002a: DSL require.role → CASL ability สตาติก mapping (research.md Decision 2) +// 'ไม่รู้จัก' DSL role → fall through ไป Level 3 (assignedUserId) check +const DSL_ROLE_TO_CASL: Record = { + Superadmin: 'system.manage_all', + OrgAdmin: 'organization.manage_users', + ContractMember: 'contract.view', + AssignedHandler: '__assigned__', // ไม่ map ไป CASL — จัดการโดย Level 3 check +}; import type { RequestWithUser } from '../../../common/interfaces/request-with-user.interface'; /** @@ -39,6 +49,8 @@ export class WorkflowTransitionGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const instanceId = request.params['id']; + // FR-002a: action \u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a DSL role check (\u0e15\u0e23\u0e27\u0e08\u0e2a\u0e2d\u0e1a requirements.roles \u0e02\u0e2d\u0e07 transition \u0e17\u0e35\u0e48\u0e15\u0e49\u0e2d\u0e07\u0e01\u0e32\u0e23\u0e17\u0e33) + const action = (request.body as { action?: string }).action ?? ''; const user = request.user; // ดึงสิทธิ์ทั้งหมดของ User จาก DB (ตาม pattern เดียวกับ RbacGuard) @@ -51,15 +63,37 @@ export class WorkflowTransitionGuard implements CanActivate { return true; } - // ดึง Instance เพื่อตรวจสอบ Context + // ดึง Instance + Definition เพื่อตรวจสอบ Context และ DSL require.role const instance = await this.instanceRepo.findOne({ where: { id: instanceId }, + relations: ['definition'], }); if (!instance) { throw new NotFoundException('Workflow Instance', instanceId); } + // FR-002a: DSL require.role → CASL ability check + // ตรวจสอบ requirements.roles ของ CompiledTransition ที่ตรงกับ action ที่ Request ขอ + // (ยังต้องผ่าน contract membership check Level 2.5) + const compiled = instance.definition?.compiled as + | CompiledWorkflow + | undefined; + const stateConfig = compiled?.states?.[instance.currentState]; + // CompiledTransition.requirements.roles — ไม่ใช่ stateConfig.require (ซึ่งไม่มี) + const requiredDslRoles: string[] = + stateConfig?.transitions?.[action]?.requirements?.roles ?? []; + let dslRoleAuthorized = false; + for (const dslRole of requiredDslRoles) { + const caslAbility = DSL_ROLE_TO_CASL[dslRole]; + if (caslAbility && caslAbility !== '__assigned__') { + if (userPermissions.includes(caslAbility)) { + dslRoleAuthorized = true; + break; + } + } + } + // Level 2: Org Admin — organization.manage_users + สังกัดองค์กรเดียวกับเอกสาร const docOrgId = instance.context?.organizationId as number | undefined; if ( @@ -99,16 +133,21 @@ export class WorkflowTransitionGuard implements CanActivate { } } - // Level 3: Assigned Handler — User นี้ถูก Assign มาให้ทำ Step นี้โดยตรง + // Level 3: Assigned Handler หรือ DSL CASL-authorized role + // FR-002a: ถ้า DSL require.role ตรงกับ CASL ability ของ User → ผ่าน + // (กรณี AssignedHandler ใน DSL → ตรวจสอบผ่าน assignedUserId ใน context) const assignedUserId = instance.context?.assignedUserId as | number | undefined; - if (assignedUserId !== undefined && user.user_id === assignedUserId) { + if ( + dslRoleAuthorized || + (assignedUserId !== undefined && user.user_id === assignedUserId) + ) { return true; } this.logger.warn( - `Unauthorized transition attempt: User ${user.user_id} on Instance ${instanceId}` + `Unauthorized transition attempt: User ${user.user_id} on Instance ${instanceId} (DSL roles: [${requiredDslRoles.join(', ')}])` ); throw new ForbiddenException({ userMessage: 'คุณไม่มีสิทธิ์ดำเนินการในขั้นตอนนี้', diff --git a/backend/src/modules/workflow-engine/workflow-engine.controller.ts b/backend/src/modules/workflow-engine/workflow-engine.controller.ts index 624eab1..456fc41 100644 --- a/backend/src/modules/workflow-engine/workflow-engine.controller.ts +++ b/backend/src/modules/workflow-engine/workflow-engine.controller.ts @@ -93,6 +93,19 @@ export class WorkflowEngineController { return this.workflowService.evaluate(dto); } + @Post('definitions/validate') + @ApiOperation({ + summary: 'FR-025: ตรวจสอบความถูกต้องของ DSL โดยไม่บันทึกข้อมูล', + }) + @ApiResponse({ + status: 200, + description: '{ valid: true } หรือ { valid: false, errors: [...] }', + }) + @RequirePermission('system.manage_all') + validateDefinition(@Body() body: { dsl: Record }) { + return this.workflowService.validateDsl(body.dsl); + } + // ================================================================= // Runtime Engine (User Actions) // ================================================================= @@ -117,6 +130,8 @@ export class WorkflowEngineController { } const userId = req.user.user_id; + // ADR-019: ใช้ publicId (UUID) แทน INT PK สำหรับ History record + const userUuid = req.user.publicId; // ตรวจ Redis ว่า Request นี้ถูกส่งมาแล้วหรือไม่ (key ผูกกับ userId ป้องกัน cross-user replay) const cacheKey = `idempotency:transition:${idempotencyKey}:${userId}`; @@ -131,7 +146,9 @@ export class WorkflowEngineController { userId, dto.comment, dto.payload, - dto.attachmentPublicIds // ADR-021: step-specific attachments + dto.attachmentPublicIds, // ADR-021: step-specific attachments + userUuid, // ADR-019: UUID สำหรับ history record + dto.versionNo // ADR-001 v1.1 FR-002: Optimistic lock ); // เก็บใน Redis 24 ชั่วโมง (86400 วินาที = 86400000 ms ใน cache-manager v7) diff --git a/backend/src/modules/workflow-engine/workflow-engine.module.ts b/backend/src/modules/workflow-engine/workflow-engine.module.ts index 9952695..9cea392 100644 --- a/backend/src/modules/workflow-engine/workflow-engine.module.ts +++ b/backend/src/modules/workflow-engine/workflow-engine.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { BullModule } from '@nestjs/bullmq'; import { makeCounterProvider, makeHistogramProvider, @@ -16,7 +17,8 @@ import { Attachment } from '../../common/file-storage/entities/attachment.entity // Services import { WorkflowDslService } from './workflow-dsl.service'; import { WorkflowEngineService } from './workflow-engine.service'; -import { WorkflowEventService } from './workflow-event.service'; // [NEW] +import { WorkflowEventService } from './workflow-event.service'; +import { WorkflowEventProcessor } from './workflow-event.processor'; // Guards import { WorkflowTransitionGuard } from './guards/workflow-transition.guard'; @@ -33,6 +35,9 @@ import { WorkflowEngineController } from './workflow-engine.controller'; WorkflowHistory, Attachment, // ADR-021: ใช้ link attachments ประจำ Step ]), + // FR-005/006: BullMQ queues สำหรับ workflow events + Dead-Letter Queue + BullModule.registerQueue({ name: 'workflow-events' }), + BullModule.registerQueue({ name: 'workflow-events-failed' }), UserModule, ], controllers: [WorkflowEngineController], @@ -40,6 +45,7 @@ import { WorkflowEngineController } from './workflow-engine.controller'; WorkflowEngineService, WorkflowDslService, WorkflowEventService, + WorkflowEventProcessor, // FR-005: BullMQ Processor + DLQ handler WorkflowTransitionGuard, // ADR-021 S1: Redlock observability — Prometheus metrics makeHistogramProvider({ @@ -52,6 +58,18 @@ import { WorkflowEngineController } from './workflow-engine.controller'; name: 'workflow_redlock_acquire_failures_total', help: 'จำนวนครั้งที่ Redlock acquire ล้มเหลวหลัง retry ครบ (Fail-closed HTTP 503)', }), + // FR-023: Per-transition metrics — labelled by workflow_code, action, outcome + makeCounterProvider({ + name: 'workflow_transitions_total', + help: 'จำนวน workflow transitions ทั้งหมด จำแนกตาม workflow_code, action และ outcome', + labelNames: ['workflow_code', 'action', 'outcome'], + }), + makeHistogramProvider({ + name: 'workflow_transition_duration_ms', + help: 'เวลาที่ใช้ในการ process workflow transition ทั้งหมด (ms) รวม Redlock + DB transaction', + labelNames: ['workflow_code'], + buckets: [50, 100, 250, 500, 1000, 2500, 5000, 10000], + }), ], exports: [WorkflowEngineService], // Export Service ให้ Module อื่น (Correspondence, RFA) เรียกใช้ }) diff --git a/backend/src/modules/workflow-engine/workflow-engine.service.spec.ts b/backend/src/modules/workflow-engine/workflow-engine.service.spec.ts index d690eaf..76f5bd1 100644 --- a/backend/src/modules/workflow-engine/workflow-engine.service.spec.ts +++ b/backend/src/modules/workflow-engine/workflow-engine.service.spec.ts @@ -35,13 +35,22 @@ import { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dt const DEFAULT_REDIS_TOKEN = 'default_IORedisModuleConnectionToken'; describe('WorkflowEngineService', () => { + let compiledModule: TestingModule; let service: WorkflowEngineService; let defRepo: Repository; let instanceRepo: Repository; + let attachmentRepo: { find: jest.Mock; update: jest.Mock }; let dslService: WorkflowDslService; let eventService: WorkflowEventService; // Mock Objects + const mockCasQueryBuilder = { + update: jest.fn().mockReturnThis(), + set: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + execute: jest.fn().mockResolvedValue({ affected: 1 }), + }; + const mockQueryRunner = { connect: jest.fn(), startTransaction: jest.fn(), @@ -52,6 +61,8 @@ describe('WorkflowEngineService', () => { findOne: jest.fn(), save: jest.fn(), update: jest.fn(), + // ADR-001 v1.1 FR-002: CAS version increment mock + createQueryBuilder: jest.fn().mockReturnValue(mockCasQueryBuilder), }, }; @@ -85,7 +96,7 @@ describe('WorkflowEngineService', () => { }); mockRedlockRelease.mockClear(); - const module: TestingModule = await Test.createTestingModule({ + compiledModule = await Test.createTestingModule({ providers: [ WorkflowEngineService, { @@ -151,14 +162,30 @@ describe('WorkflowEngineService', () => { inc: jest.fn(), }, }, + // FR-023: Per-transition metrics mocks + { + provide: 'PROM_METRIC_WORKFLOW_TRANSITIONS_TOTAL', + useValue: { + labels: jest.fn().mockReturnThis(), + inc: jest.fn(), + }, + }, + { + provide: 'PROM_METRIC_WORKFLOW_TRANSITION_DURATION_MS', + useValue: { + labels: jest.fn().mockReturnThis(), + observe: jest.fn(), + }, + }, ], }).compile(); - service = module.get(WorkflowEngineService); - defRepo = module.get(getRepositoryToken(WorkflowDefinition)); - instanceRepo = module.get(getRepositoryToken(WorkflowInstance)); - dslService = module.get(WorkflowDslService); - eventService = module.get(WorkflowEventService); + service = compiledModule.get(WorkflowEngineService); + defRepo = compiledModule.get(getRepositoryToken(WorkflowDefinition)); + instanceRepo = compiledModule.get(getRepositoryToken(WorkflowInstance)); + attachmentRepo = compiledModule.get(getRepositoryToken(Attachment)); + dslService = compiledModule.get(WorkflowDslService); + eventService = compiledModule.get(WorkflowEventService); }); it('should be defined', () => { @@ -563,11 +590,13 @@ describe('WorkflowEngineService', () => { id: 'inst-1', currentState: 'PENDING_REVIEW', status: WorkflowStatus.ACTIVE, - definition: { compiled: mockCompiledWorkflow }, + definition: { compiled: mockCompiledWorkflow, workflow_code: 'WF01' }, context: {}, + versionNo: 1, }); mockQueryRunner.manager.save.mockResolvedValue({ id: 'history-1' }); mockQueryRunner.manager.update.mockResolvedValue({ affected: 1 }); + mockCasQueryBuilder.execute.mockResolvedValue({ affected: 1 }); mockDslService.evaluate.mockReturnValue({ nextState: 'APPROVED', events: [], @@ -585,4 +614,283 @@ describe('WorkflowEngineService', () => { }); }); }); + + // ============================================================ + // T024: ADR-001 v1.1 FR-002 — Optimistic Lock Tests + // ============================================================ + describe('Optimistic Lock (FR-002)', () => { + const baseInstance = { + id: 'inst-opt-1', + currentState: 'PENDING_REVIEW', + status: WorkflowStatus.ACTIVE, + definition: { compiled: mockCompiledWorkflow, workflow_code: 'WF01' }, + context: {}, + versionNo: 5, + }; + + it('T024a: should throw ConflictException (409) when clientVersionNo does not match current versionNo (fast-fail)', async () => { + // Arrange: DB มี version_no=5, client ส่ง version_no=3 (ล้าสมัย) + (instanceRepo.findOne as jest.Mock).mockResolvedValue({ + id: 'inst-opt-1', + versionNo: 5, + }); + + // Act + Assert + await expect( + service.processTransition( + 'inst-opt-1', + 'APPROVE', + 1, + undefined, + {}, + undefined, + 'user-uuid-123', + 3 // clientVersionNo ล้าสมัย + ) + ).rejects.toThrow(ConflictException); + + // Fast-fail: Redlock ต้องไม่ถูกเรียก (ผ่าน check ก่อน acquire) + expect(mockRedlockAcquire).not.toHaveBeenCalled(); + }); + + it('T024b: should pass fast-fail and proceed when clientVersionNo matches current versionNo', async () => { + // Arrange: clientVersionNo ตรงกับ DB + (instanceRepo.findOne as jest.Mock).mockResolvedValue({ + id: 'inst-opt-1', + currentState: 'PENDING_REVIEW', + versionNo: 5, + }); + mockQueryRunner.manager.findOne.mockResolvedValue({ + ...baseInstance, + versionNo: 5, + }); + mockQueryRunner.manager.save.mockResolvedValue({ id: 'history-1' }); + mockCasQueryBuilder.execute.mockResolvedValue({ affected: 1 }); + mockDslService.evaluate.mockReturnValue({ + nextState: 'APPROVED', + events: [], + }); + + // Act + const result = await service.processTransition( + 'inst-opt-1', + 'APPROVE', + 1, + undefined, + {}, + undefined, + 'user-uuid-123', + 5 // clientVersionNo ตรง + ); + + // Assert: สำเร็จ + คืน versionNo ใหม่ + expect(result.success).toBe(true); + expect(result.versionNo).toBe(6); // 5 + 1 + expect(mockRedlockAcquire).toHaveBeenCalled(); + }); + + it('T024c: should throw ConflictException when CAS update returns affected=0 (TOCTOU edge case)', async () => { + // Arrange: fast-fail ผ่าน (ไม่ส่ง clientVersionNo), แต่ CAS ล้มเหลว + (instanceRepo.findOne as jest.Mock).mockResolvedValue({ + id: 'inst-opt-1', + currentState: 'PENDING_REVIEW', + versionNo: 5, + }); + mockQueryRunner.manager.findOne.mockResolvedValue({ + ...baseInstance, + versionNo: 5, + }); + mockQueryRunner.manager.save.mockResolvedValue({ id: 'history-1' }); + // CAS: เกิด TOCTOU — version_no ถูกเปลี่ยนระหว่าง Redlock acquire กับ CAS update + mockCasQueryBuilder.execute.mockResolvedValue({ affected: 0 }); + mockDslService.evaluate.mockReturnValue({ + nextState: 'APPROVED', + events: [], + }); + + // Act + Assert + await expect( + service.processTransition( + 'inst-opt-1', + 'APPROVE', + 1, + undefined, + {}, + undefined + // ไม่ส่ง clientVersionNo — TOCTOU ถูกตรวจโดย CAS layer + ) + ).rejects.toThrow(ConflictException); + + expect(mockQueryRunner.rollbackTransaction).toHaveBeenCalled(); + expect(mockQueryRunner.commitTransaction).not.toHaveBeenCalled(); + }); + + it('T024d: should rollback attachments to temp when DB transaction fails (FR-019)', async () => { + // Arrange: commit ล้มเหลว — คาดว่า attachments จะถูก revert กลับเป็น temp + (instanceRepo.findOne as jest.Mock).mockResolvedValue(null); // no pre-check needed (no attachment state) + mockQueryRunner.manager.findOne.mockResolvedValue({ + ...baseInstance, + versionNo: 5, + }); + mockQueryRunner.manager.save.mockResolvedValue({ id: 'history-1' }); + // CAS สำเร็จ + mockCasQueryBuilder.execute.mockResolvedValue({ affected: 1 }); + // commitTransaction ล้มเหลว + mockQueryRunner.commitTransaction.mockRejectedValueOnce( + new Error('DB connection lost') + ); + mockDslService.evaluate.mockReturnValue({ + nextState: 'APPROVED', + events: [], + }); + + // Act + Assert + await expect( + service.processTransition( + 'inst-opt-1', + 'APPROVE', + 1, + undefined, + {}, + ['att-rollback-1', 'att-rollback-2'] // แนบไฟล์ 2 ไฟล์ + ) + ).rejects.toThrow(Error); + + // FR-019: attachmentRepo.update ต้องถูกเรียกเพื่อ revert ไฟล์กลับเป็น temp + expect(attachmentRepo.update).toHaveBeenCalledWith( + expect.objectContaining({ + publicId: ['att-rollback-1', 'att-rollback-2'], + }), + expect.objectContaining({ isTemporary: true }) + ); + }); + }); + + // ============================================================ + // T048: ADR-001 FR-007 — DSL Redis Cache Invalidation Tests + // ============================================================ + describe('DSL Redis Cache Invalidation (FR-007, SC-005)', () => { + it('T048a: update() should invalidate cache when DSL changes', async () => { + // Arrange + const mockDef = { + id: 'def-cache-1', + workflow_code: 'RFA_V1', + version: 2, + is_active: false, + dsl: {}, + compiled: {}, + }; + (defRepo.findOne as jest.Mock).mockResolvedValue(mockDef); + (defRepo.save as jest.Mock).mockResolvedValue({ ...mockDef, version: 2 }); + mockDslService.compile.mockReturnValue(mockCompiledWorkflow); + + const cacheManager = compiledModule.get<{ + del: jest.Mock; + set: jest.Mock; + get: jest.Mock; + }>(CACHE_MANAGER); + + // Act + await service.update('def-cache-1', { + dsl: { + workflow: 'RFA_V1', + states: [], + } as unknown as import('./dto/create-workflow-definition.dto').CreateWorkflowDefinitionDto['dsl'], + }); + + // Assert: cache del เรียกด้วย version key + expect(cacheManager.del).toHaveBeenCalledWith('wf:def:RFA_V1:2'); + // Assert: re-cache เรียกหลัง del + expect(cacheManager.set).toHaveBeenCalledWith( + 'wf:def:RFA_V1:2', + expect.any(Object), + 3_600_000 + ); + }); + + it('T048b: update() should invalidate active pointer when is_active toggles to true', async () => { + // Arrange: definition เดิม is_active = false + const mockDef = { + id: 'def-cache-2', + workflow_code: 'TRANSMITTAL_V1', + version: 1, + is_active: false, + dsl: {}, + compiled: {}, + }; + (defRepo.findOne as jest.Mock).mockResolvedValue(mockDef); + (defRepo.save as jest.Mock).mockResolvedValue({ + ...mockDef, + is_active: true, + }); + + const cacheManager = compiledModule.get<{ + del: jest.Mock; + set: jest.Mock; + get: jest.Mock; + }>(CACHE_MANAGER); + + // Act: activate definition + await service.update('def-cache-2', { is_active: true }); + + // Assert: active pointer ถูกลบออกจาก cache + expect(cacheManager.del).toHaveBeenCalledWith( + 'wf:def:TRANSMITTAL_V1:active' + ); + }); + + it('T048c: createDefinition() should set cache with version key after save', async () => { + // Arrange + (defRepo.findOne as jest.Mock).mockResolvedValue({ version: 3 }); + (defRepo.create as jest.Mock).mockReturnValue({ + workflow_code: 'WF_CACHE', + version: 4, + }); + (defRepo.save as jest.Mock).mockResolvedValue({ + workflow_code: 'WF_CACHE', + version: 4, + }); + mockDslService.compile.mockReturnValue(mockCompiledWorkflow); + const cacheManager = compiledModule.get<{ + del: jest.Mock; + set: jest.Mock; + get: jest.Mock; + }>(CACHE_MANAGER); + + // Act + await service.createDefinition({ + workflow_code: 'WF_CACHE', + dsl: {}, + } as import('./dto/create-workflow-definition.dto').CreateWorkflowDefinitionDto); + + // Assert: cache set ด้วย version key + expect(cacheManager.set).toHaveBeenCalledWith( + 'wf:def:WF_CACHE:4', + expect.objectContaining({ workflow_code: 'WF_CACHE', version: 4 }), + 3_600_000 + ); + }); + + it('T048d: getDefinitionById() should return from cache on cache hit', async () => { + // Arrange: cache มีข้อมูลอยู่แล้ว + const cachedDef = { + id: 'def-hit-1', + workflow_code: 'CACHED_WF', + version: 1, + }; + const cacheManager = compiledModule.get<{ + del: jest.Mock; + set: jest.Mock; + get: jest.Mock; + }>(CACHE_MANAGER); + cacheManager.get.mockResolvedValueOnce(cachedDef); + + // Act + const result = await service.getDefinitionById('def-hit-1'); + + // Assert: ไม่ต้องออก DB + expect(result).toEqual(cachedDef); + expect(defRepo.findOne).not.toHaveBeenCalled(); + }); + }); }); diff --git a/backend/src/modules/workflow-engine/workflow-engine.service.ts b/backend/src/modules/workflow-engine/workflow-engine.service.ts index 0321abe..2b1f6b7 100644 --- a/backend/src/modules/workflow-engine/workflow-engine.service.ts +++ b/backend/src/modules/workflow-engine/workflow-engine.service.ts @@ -32,7 +32,11 @@ import { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dt import { EvaluateWorkflowDto } from './dto/evaluate-workflow.dto'; import { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto'; import { WorkflowHistoryItemDto } from './dto/workflow-history-item.dto'; -import { CompiledWorkflow, WorkflowDslService } from './workflow-dsl.service'; +import { + CompiledWorkflow, + RawWorkflowDSL, + WorkflowDslService, +} from './workflow-dsl.service'; import { WorkflowEventService } from './workflow-event.service'; // [NEW] Import Event Service // Legacy Interface (Backward Compatibility) @@ -79,7 +83,12 @@ export class WorkflowEngineService { @InjectMetric('workflow_redlock_acquire_duration_ms') private readonly redlockAcquireDuration: Histogram, @InjectMetric('workflow_redlock_acquire_failures_total') - private readonly redlockAcquireFailures: Counter + private readonly redlockAcquireFailures: Counter, + // FR-023: Per-transition metrics — labelled by workflow_code, action, outcome + @InjectMetric('workflow_transitions_total') + private readonly transitionsTotal: Counter, + @InjectMetric('workflow_transition_duration_ms') + private readonly transitionDuration: Histogram ) { // ADR-021 Clarify Q2 (C1): Redlock Fail-closed // Retry 3 ครั้ง × 500ms เพิ่ม jitter → ถ้ายังไม่ได้ throw HTTP 503 @@ -95,6 +104,30 @@ export class WorkflowEngineService { // [PART 1] Definition Management (Phase 6A) // ================================================================= + /** + * FR-025: ตรวจสอบ DSL โดยไม่บันทึก — ใช้สำหรับ inline validation ใน Admin Editor + */ + validateDsl( + dsl: Record + ): + | { valid: true } + | { valid: false; errors: { path: string; message: string }[] } { + try { + this.dslService.compile(dsl as unknown as RawWorkflowDSL); + return { valid: true }; + } catch (error: unknown) { + return { + valid: false, + errors: [ + { + path: '', + message: error instanceof Error ? error.message : String(error), + }, + ], + }; + } + } + /** * สร้างหรืออัปเดต Workflow Definition ใหม่ (Auto Versioning) */ @@ -122,6 +155,12 @@ export class WorkflowEngineService { }); const saved = await this.workflowDefRepo.save(entity); + // T044: Cache definition per version (TTL 1h, SC-005) + await this.cacheManager.set( + `wf:def:${saved.workflow_code}:${saved.version}`, + saved, + 3_600_000 + ); this.logger.log( `Created Workflow Definition: ${saved.workflow_code} v${saved.version}` ); @@ -155,10 +194,30 @@ export class WorkflowEngineService { } } + const prevIsActive = definition.is_active; if (dto.is_active !== undefined) definition.is_active = dto.is_active; if (dto.workflow_code) definition.workflow_code = dto.workflow_code; - return this.workflowDefRepo.save(definition); + const updated = await this.workflowDefRepo.save(definition); + + // T045: Invalidate version cache เมื่อ DSL เปลี่ยน + if (dto.dsl) { + await this.cacheManager.del( + `wf:def:${updated.workflow_code}:${updated.version}` + ); + } + // T045: Invalidate active pointer เมื่อ is_active เปลี่ยน + if (dto.is_active !== undefined && dto.is_active !== prevIsActive) { + await this.cacheManager.del(`wf:def:${updated.workflow_code}:active`); + } + // T045: Re-cache updated definition + await this.cacheManager.set( + `wf:def:${updated.workflow_code}:${updated.version}`, + updated, + 3_600_000 + ); + + return updated; } /** @@ -181,10 +240,17 @@ export class WorkflowEngineService { * ดึง Workflow Definition ตาม ID หรือ Code */ async getDefinitionById(id: string): Promise { + // T046: Read-through cache (TTL 1h, SC-005) + const cacheKey = `wf:def:id:${id}`; + const cached = await this.cacheManager.get(cacheKey); + if (cached) return cached; + const definition = await this.workflowDefRepo.findOne({ where: { id } }); if (!definition) { throw new NotFoundException('Workflow Definition', id); } + + await this.cacheManager.set(cacheKey, definition, 3_600_000); return definition; } @@ -317,7 +383,7 @@ export class WorkflowEngineService { : []; return { - id: instance.id, + id: instance.id, // publicId (UUID) ของ workflow instance currentState: instance.currentState, availableActions, }; @@ -333,11 +399,49 @@ export class WorkflowEngineService { comment?: string, payload: Record = {}, // ADR-021: publicIds ของไฟล์แนบประจำ Step นี้ (Two-Phase upload ก่อนแล้ว) - attachmentPublicIds?: string[] + attachmentPublicIds?: string[], + // ADR-019: UUID ของ User สำหรับ history record (ไม่ expose INT PK) + userUuid?: string, + // ADR-001 v1.1 FR-002: Optimistic lock — Client ส่งมาเพื่อป้องกัน Double-approval + clientVersionNo?: number ) { + // FR-022/023: เริ่มจับเวลาทั้ง method เพื่อบันทึก latency metric + const startMs = Date.now(); + let outcome: + | 'success' + | 'conflict' + | 'forbidden' + | 'validation_error' + | 'system_error' = 'system_error'; + let workflowCode = 'unknown'; + let fromState: string | undefined; + let toState: string | undefined; const hasAttachments = attachmentPublicIds !== undefined && attachmentPublicIds.length > 0; + // ============================================================== + // ADR-001 v1.1 FR-002: Fast-fail Optimistic Lock Check (ก่อน Redlock) + // ลดภาระ Redlock สำหรับ Client ที่ส่ง version_no ล้าสมัยมา + // ============================================================== + if (clientVersionNo !== undefined) { + const current = await this.instanceRepo.findOne({ + where: { id: instanceId }, + select: ['id', 'versionNo'], + }); + if (!current) { + throw new NotFoundException('Workflow Instance', instanceId); + } + if (current.versionNo !== clientVersionNo) { + outcome = 'conflict'; + throw new ConflictException( + 'WORKFLOW_VERSION_CONFLICT', + `Fast-fail: expected version_no=${clientVersionNo}, actual=${current.versionNo}`, + 'เอกสารถูกอนุมัติโดยผู้อื่นแล้ว กรุณารีเฟรชและลองใหม่', + ['รีเฟรชหน้าแล้วดูสถานะล่าสุดก่อนดำเนินการ'] + ); + } + } + // ============================================================== // ADR-021 Clarify Q1 (C3): ตรวจสถานะก่อน acquire Redlock // อนุญาตให้แนบไฟล์เฉพาะในสถานะ PENDING_REVIEW / PENDING_APPROVAL @@ -453,8 +557,10 @@ export class WorkflowEngineService { context ); - const fromState = instance.currentState; - const toState = evaluation.nextState; + fromState = instance.currentState; + toState = evaluation.nextState; + // FR-023: บันทึก workflowCode สำหรับ metric labels + workflowCode = instance.definition?.workflow_code ?? 'unknown'; // 3. อัปเดต Instance instance.currentState = toState; @@ -474,6 +580,8 @@ export class WorkflowEngineService { toState, action, actionByUserId: userId, + // ADR-019 FR-003: UUID ของ User สำหรับ API Response (INT PK ไม่ expose) + actionByUserUuid: userUuid, comment, metadata: { events: evaluation.events, @@ -516,6 +624,27 @@ export class WorkflowEngineService { } } + // ADR-001 v1.1 FR-002: CAS version increment หลัง commit ใน DB transaction + // UPDATE จะล้มเหลว (affected=0) ถ้า version_no ถูกเปลี่ยนระหว่างนี้ (TOCTOU edge case) + const casResult = await queryRunner.manager + .createQueryBuilder() + .update(WorkflowInstance) + .set({ versionNo: () => 'version_no + 1' }) + .where('id = :id AND version_no = :expected', { + id: instanceId, + expected: instance.versionNo, + }) + .execute(); + + if ((casResult.affected ?? 0) === 0) { + throw new ConflictException( + 'WORKFLOW_VERSION_CONFLICT', + 'version_no changed between Redlock acquisition and CAS update (TOCTOU edge case)', + 'เกิด Conflict กรุณารีเฟรชและลองใหม่', + ['รีเฟรชหน้า', 'ลองดำเนินการอีกครั้ง'] + ); + } + await queryRunner.commitTransaction(); // ADR-021 T043: Invalidate Workflow History cache หลัง transition สำเร็จ @@ -536,23 +665,85 @@ export class WorkflowEngineService { void this.eventService.dispatchEvents( instance.id, evaluation.events, - context + context, + workflowCode // FR-005: DLQ notification \u0e43\u0e0a\u0e49 workflowCode \u0e23\u0e30\u0e1a\u0e38\u0e1a\u0e23\u0e34\u0e1a\u0e17\u0e18\u0e34\u0e4c Ops ); } + outcome = 'success'; + // FR-014 T014: คืน versionNo ที่ increment แล้ว ให้ Client เก็บไว้สำหรับ request ถัดไป + const newVersionNo = instance.versionNo + 1; + return { success: true, + previousState: fromState, nextState: toState, events: evaluation.events, isCompleted: instance.status === WorkflowStatus.COMPLETED, + versionNo: newVersionNo, }; } catch (err) { await queryRunner.rollbackTransaction(); + + // FR-019: Rollback file attachments กลับเป็น temporary เมื่อ DB transaction ล้มเหลว + // ไฟล์บน disk ยังคงอยู่ที่ permanent storage; cleanup job จะจัดการหลัง 24h TTL + if ( + hasAttachments && + attachmentPublicIds && + attachmentPublicIds.length > 0 + ) { + await this.attachmentRepo + .update( + { publicId: In(attachmentPublicIds), uploadedByUserId: userId }, + { + isTemporary: true, + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + } + ) + .catch((rollbackErr: unknown) => + this.logger.error( + `FR-019 Attachment rollback failed for ${instanceId}: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}` + ) + ); + this.logger.warn( + `FR-019: Reverted ${attachmentPublicIds.length} attachment(s) to temp for instance ${instanceId} after DB failure` + ); + } + + // จำแนก outcome สำหรับ metric label + if (err instanceof ConflictException) outcome = 'conflict'; + else if ((err as { status?: number }).status === 403) + outcome = 'forbidden'; + else if (err instanceof WorkflowException) outcome = 'validation_error'; + this.logger.error( `Transition Failed for ${instanceId}: ${(err as Error).message}` ); throw err; } finally { + const durationMs = Date.now() - startMs; + // FR-023: บันทึก transition duration histogram + this.transitionDuration + .labels({ workflow_code: workflowCode }) + .observe(durationMs); + // FR-023: บันทึก transition counter ตาม outcome + this.transitionsTotal + .labels({ workflow_code: workflowCode, action, outcome }) + .inc(); + // FR-022: Structured log entry ทุก transition (success/failure/conflict) + this.logger.log( + JSON.stringify({ + instanceId, + action, + fromState, + toState, + userUuid, + durationMs, + outcome, + workflowCode, + }) + ); + await queryRunner.release(); // ADR-021 C1: ปล่อย Redlock เสมอ (non-blocking หาก release ผิดพลาด) lock.release().catch((e: unknown) => { diff --git a/backend/src/modules/workflow-engine/workflow-event.processor.spec.ts b/backend/src/modules/workflow-engine/workflow-event.processor.spec.ts new file mode 100644 index 0000000..7fd3d81 --- /dev/null +++ b/backend/src/modules/workflow-engine/workflow-event.processor.spec.ts @@ -0,0 +1,165 @@ +// File: src/modules/workflow-engine/workflow-event.processor.spec.ts +// T026: Unit tests for WorkflowEventProcessor DLQ + n8n webhook (FR-005, FR-006) + +import { Test, TestingModule } from '@nestjs/testing'; +import { getQueueToken } from '@nestjs/bullmq'; +import { WorkflowEventProcessor } from './workflow-event.processor'; +import type { WorkflowEventJobData } from './workflow-event.processor'; +import type { Job } from 'bullmq'; + +// Mock global fetch สำหรับ n8n webhook +const mockFetch = jest.fn(); +global.fetch = mockFetch; + +describe('WorkflowEventProcessor', () => { + let processor: WorkflowEventProcessor; + let failedQueue: { add: jest.Mock }; + + const makeJob = ( + overrides: Partial<{ + id: string; + attemptsMade: number; + opts: { attempts: number }; + data: Record; + }> = {} + ) => + ({ + id: 'job-001', + attemptsMade: 3, + opts: { attempts: 3 }, + data: { + instanceId: 'inst-wf-1', + events: [{ type: 'notify', target: 'admin', template: 'APPROVED' }], + context: {}, + workflowCode: 'RFA_V1', + }, + ...overrides, + }) as unknown as Job; + + beforeEach(async () => { + failedQueue = { add: jest.fn().mockResolvedValue(undefined) }; + mockFetch.mockReset(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + WorkflowEventProcessor, + { + provide: getQueueToken('workflow-events-failed'), + useValue: failedQueue, + }, + ], + }).compile(); + + processor = module.get(WorkflowEventProcessor); + }); + + afterEach(() => { + delete process.env['N8N_WEBHOOK_URL']; + }); + + describe('onJobFailed()', () => { + it('T026a: should add dead-letter job to workflow-events-failed queue when attempts exhausted', async () => { + // Arrange: job.attemptsMade === job.opts.attempts (หมด retry) + const job = makeJob({ attemptsMade: 3, opts: { attempts: 3 } }); + const error = new Error('Notification service timeout'); + + // Act + await processor.onJobFailed(job, error); + + // Assert: ส่งไปยัง DLQ + expect(failedQueue.add).toHaveBeenCalledWith( + 'dead-letter', + expect.objectContaining({ + originalJobId: 'job-001', + queue: 'workflow-events', + error: 'Notification service timeout', + data: expect.objectContaining({ instanceId: 'inst-wf-1' }), + }) + ); + }); + + it('T026b: should NOT add to DLQ when job still has retry attempts remaining', async () => { + // Arrange: attempt 1 of 3 — ยังมี retry เหลือ + const job = makeJob({ attemptsMade: 1, opts: { attempts: 3 } }); + const error = new Error('Temporary error'); + + // Act + await processor.onJobFailed(job, error); + + // Assert: ไม่ส่ง DLQ + expect(failedQueue.add).not.toHaveBeenCalled(); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('T026c: should POST to n8n webhook when N8N_WEBHOOK_URL is configured', async () => { + // Arrange: ตั้งค่า webhook URL + process.env['N8N_WEBHOOK_URL'] = 'https://n8n.example.com/webhook/dlq'; + mockFetch.mockResolvedValue({ ok: true }); + + const job = makeJob({ attemptsMade: 3, opts: { attempts: 3 } }); + const error = new Error('Service down'); + + // Act + await processor.onJobFailed(job, error); + + // Assert: เรียก n8n webhook + expect(mockFetch).toHaveBeenCalledWith( + 'https://n8n.example.com/webhook/dlq', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: expect.stringContaining('"event":"workflow_event_failed"'), + }) + ); + + // Body ต้องมี workflowCode + instanceId + const callArgs = mockFetch.mock.calls[0] as [string, RequestInit]; + const body = JSON.parse(callArgs[1].body as string) as Record< + string, + unknown + >; + expect(body).toMatchObject({ + event: 'workflow_event_failed', + jobId: 'job-001', + workflowCode: 'RFA_V1', + instanceId: 'inst-wf-1', + error: 'Service down', + }); + }); + + it('T026d: should warn (not throw) when N8N_WEBHOOK_URL is not set', async () => { + // Arrange: ไม่ตั้ง env var + delete process.env['N8N_WEBHOOK_URL']; + + const job = makeJob({ attemptsMade: 3, opts: { attempts: 3 } }); + const error = new Error('Error'); + + // Act — ต้องไม่ throw + await expect(processor.onJobFailed(job, error)).resolves.toBeUndefined(); + + // DLQ ยังต้องถูกเรียก — แค่ไม่ call webhook + expect(failedQueue.add).toHaveBeenCalled(); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('T026e: should continue without throwing when DLQ add fails', async () => { + // Arrange: DLQ queue ล้มเหลว — ไม่ควร throw ออกมา + failedQueue.add.mockRejectedValueOnce(new Error('Redis DLQ down')); + + const job = makeJob({ attemptsMade: 3, opts: { attempts: 3 } }); + const error = new Error('Original error'); + + // Act: ต้อง resolve ปกติ ไม่ throw + await expect(processor.onJobFailed(job, error)).resolves.toBeUndefined(); + }); + }); + + describe('process()', () => { + it('T026f: should process notify event without error', async () => { + const job = makeJob(); + + // Act — ต้อง resolve ปกติ + await expect(processor.process(job)).resolves.toBeUndefined(); + }); + }); +}); diff --git a/backend/src/modules/workflow-engine/workflow-event.processor.ts b/backend/src/modules/workflow-engine/workflow-event.processor.ts new file mode 100644 index 0000000..10ec170 --- /dev/null +++ b/backend/src/modules/workflow-engine/workflow-event.processor.ts @@ -0,0 +1,133 @@ +// File: src/modules/workflow-engine/workflow-event.processor.ts +// FR-005/FR-006: BullMQ Processor สำหรับ workflow-events queue พร้อม Dead-Letter Queue + +import { + Processor, + WorkerHost, + OnWorkerEvent, + InjectQueue, +} from '@nestjs/bullmq'; +import { Logger } from '@nestjs/common'; +import { Job, Queue } from 'bullmq'; +import { RawEvent } from './workflow-dsl.service'; + +export interface WorkflowEventJobData { + instanceId: string; + events: RawEvent[]; + context: Record; + workflowCode?: string; +} + +@Processor('workflow-events', { + concurrency: 5, + limiter: { max: 100, duration: 60_000 }, +}) +export class WorkflowEventProcessor extends WorkerHost { + private readonly logger = new Logger(WorkflowEventProcessor.name); + + constructor( + // FR-006: Queue สำหรับ Dead-Letter (jobs ที่หมด retry) + @InjectQueue('workflow-events-failed') + private readonly failedQueue: Queue + ) { + super(); + } + + // ADR-008: ประมวลผล workflow event job + process(job: Job): Promise { + const { instanceId, events } = job.data; + this.logger.log( + `Processing ${events.length} event(s) for Instance ${instanceId} (Job: ${job.id})` + ); + + // ประมวลผลแต่ละ event (throw เพื่อให้ BullMQ retry อัตโนมัติ) + for (const event of events) { + this.processSingleEvent(instanceId, event, job.data.context); + } + return Promise.resolve(); + } + + // FR-006: Dead-Letter Queue handler — เรียกเมื่อ job หมด retry ทั้งหมด + @OnWorkerEvent('failed') + async onJobFailed( + job: Job, + error: Error + ): Promise { + const maxAttempts = job.opts.attempts ?? 3; + if ((job.attemptsMade ?? 0) < maxAttempts) { + // ยังมี retry เหลือ — ไม่ต้องส่ง DLQ + return; + } + + this.logger.error( + `Job ${job.id} exhausted all ${maxAttempts} retries for Instance ${job.data.instanceId}: ${error.message}` + ); + + // ส่งไปยัง Dead-Letter Queue + await this.failedQueue + .add('dead-letter', { + originalJobId: job.id, + queue: 'workflow-events', + data: job.data, + failedAt: new Date().toISOString(), + error: error.message, + }) + .catch((dlqErr: unknown) => + this.logger.error( + `Failed to add job ${job.id} to DLQ: ${dlqErr instanceof Error ? dlqErr.message : String(dlqErr)}` + ) + ); + + // แจ้ง Ops ผ่าน n8n webhook (ถ้าตั้งค่าไว้) + const webhookUrl = process.env['N8N_WEBHOOK_URL']; + if (webhookUrl) { + await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + event: 'workflow_event_failed', + jobId: job.id, + workflowCode: job.data.workflowCode, + instanceId: job.data.instanceId, + error: error.message, + timestamp: new Date().toISOString(), + }), + }).catch((webhookErr: unknown) => { + // Warning เท่านั้น — ไม่ throw เพื่อไม่กระทบ DLQ add ที่สำเร็จแล้ว + this.logger.warn( + `n8n webhook failed for job ${job.id}: ${webhookErr instanceof Error ? webhookErr.message : String(webhookErr)}` + ); + }); + } else { + this.logger.warn( + `N8N_WEBHOOK_URL not configured — DLQ job created without ops notification (job: ${job.id})` + ); + } + } + + // --- Private Handlers --- + + private processSingleEvent( + instanceId: string, + event: RawEvent, + _context: Record + ): void { + switch (event.type) { + case 'notify': + this.logger.log( + `[NOTIFY] Instance ${instanceId} → target: "${event.target}" | template: "${event.template}"` + ); + break; + case 'webhook': + this.logger.log( + `[WEBHOOK] Instance ${instanceId} → url: "${event.target}"` + ); + break; + case 'auto_action': + this.logger.log(`[AUTO_ACTION] Instance ${instanceId}`); + break; + default: + this.logger.warn(`Unknown event type: ${event.type} for ${instanceId}`); + } + } +} diff --git a/backend/src/modules/workflow-engine/workflow-event.service.ts b/backend/src/modules/workflow-engine/workflow-event.service.ts index df88ebb..41e3776 100644 --- a/backend/src/modules/workflow-engine/workflow-event.service.ts +++ b/backend/src/modules/workflow-engine/workflow-event.service.ts @@ -1,6 +1,8 @@ // File: src/modules/workflow-engine/workflow-event.service.ts import { Injectable, Logger } from '@nestjs/common'; +import { InjectQueue } from '@nestjs/bullmq'; +import { Queue } from 'bullmq'; import { RawEvent } from './workflow-dsl.service'; // Interface สำหรับ External Services ที่จะมารับ Event ต่อ @@ -19,81 +21,47 @@ export interface WorkflowEventHandler { export class WorkflowEventService { private readonly logger = new Logger(WorkflowEventService.name); - // สามารถ Inject NotificationService หรือ HttpService เข้ามาได้ตรงนี้ - // constructor(private readonly notificationService: NotificationService) {} + constructor( + // ADR-008: ใช้ BullMQ queue แทน inline processing เพื่อ Retry + DLQ (FR-005) + @InjectQueue('workflow-events') + private readonly workflowEventQueue: Queue + ) {} /** - * ประมวลผลรายการ Events ที่เกิดจากการเปลี่ยนสถานะ + * เพิ่ม Job ลงใน workflow-events queue (ADR-008: Async ไม่ Block Response) + * Processor: WorkflowEventProcessor (workflow-event.processor.ts) */ dispatchEvents( instanceId: string, events: RawEvent[], - context: Record - ) { + context: Record, + workflowCode?: string + ): void { if (!events || events.length === 0) return; this.logger.log( - `Dispatching ${events.length} events for Instance ${instanceId}` + `Enqueuing ${events.length} event(s) for Instance ${instanceId} → workflow-events queue` ); - // ทำแบบ Async ไม่รอผล (Fire-and-forget) เพื่อไม่ให้กระทบ Response Time ของ User - void Promise.allSettled( - events.map((event) => this.processSingleEvent(instanceId, event, context)) - ).then((results) => { - // Log errors if any - results.forEach((res, idx) => { - if (res.status === 'rejected') { - this.logger.error( - `Failed to process event [${idx}]: ${String(res.reason)}` - ); + // ADR-008: Fire-and-forget — ไม่ await เพื่อไม่กระทบ Response Time + // WorkflowEventProcessor จะประมวลผลและ retry อัตโนมัติ (3 retries, exponential backoff) + void this.workflowEventQueue + .add( + 'process-events', + { instanceId, events, context, workflowCode }, + { + attempts: 3, + backoff: { type: 'exponential', delay: 500 }, + removeOnComplete: { age: 86_400 }, // เก็บ 24h + removeOnFail: false, // เก็บไว้ใน Bull Board + DLQ } - }); - }); - } - - private async processSingleEvent( - instanceId: string, - event: RawEvent, - context: Record - ) { - await Promise.resolve(); - try { - switch (event.type) { - case 'notify': - this.handleNotify(event, context); - break; - case 'webhook': - this.handleWebhook(event, context); - break; - case 'auto_action': - // Logic สำหรับ Auto Transition (เช่น ถ้าผ่านเงื่อนไข ให้ไปต่อเลย) - this.logger.log(`Auto Action triggered for ${instanceId}`); - break; - default: - this.logger.warn(`Unknown event type: ${event.type}`); - } - } catch (error) { - this.logger.error( - `Error processing event ${event.type}: ${String(error)}` + ) + .catch((err: unknown) => + this.logger.error( + `Failed to enqueue workflow events for ${instanceId}: ${ + err instanceof Error ? err.message : String(err) + }` + ) ); - throw error; - } - } - - // --- Handlers --- - - private handleNotify(event: RawEvent, _context: Record) { - // Mockup: ในของจริงจะเรียก NotificationService.send() - // const recipients = this.resolveRecipients(event.target, context); - this.logger.log( - `[EVENT] Notify target: "${event.target}" | Template: "${event.template}"` - ); - } - - private handleWebhook(event: RawEvent, _context: Record) { - // Mockup: เรียก HttpService.post() - this.logger.log( - `[EVENT] Webhook to: "${event.target}" | Payload: ${JSON.stringify(event.payload)}` - ); } } diff --git a/docs/agents/domain.md b/docs/agents/domain.md new file mode 100644 index 0000000..879f9ba --- /dev/null +++ b/docs/agents/domain.md @@ -0,0 +1,41 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root, or +- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. +- **`specs/06-Decision-Records/`** — read ADRs that touch the area you're about to work in. This repo uses `specs/` instead of `docs/` for all documentation. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-with-docs`) creates them lazily when terms or decisions actually get resolved. + +## File structure + +Single-context repo (this repo): + +``` +/ +├── CONTEXT.md (if it exists) +├── specs/ +│ ├── 00-overview/ +│ ├── 01-requirements/ +│ ├── 02-architecture/ +│ ├── 03-Data-and-Storage/ +│ ├── 04-Infrastructure-OPS/ +│ ├── 05-Engineering-Guidelines/ +│ └── 06-Decision-Records/ ← ADRs live here +└── src/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `specs/00-overview/00-02-glossary.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-with-docs`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR in `specs/06-Decision-Records/`, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-001 (unified workflow engine) — but worth reopening because…_ diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md new file mode 100644 index 0000000..31287a1 --- /dev/null +++ b/docs/agents/issue-tracker.md @@ -0,0 +1,23 @@ +# Issue tracker: Gitea + +Issues and PRDs for this repo live in the self-hosted Gitea instance at git.np-dms.work:2222. Use the `gh` CLI with custom host configuration for all operations. + +## Conventions + +- **Configure `gh` for Gitea**: Run `gh auth login --hostname git.np-dms.work:2222` to authenticate +- **Create an issue**: `gh issue create --hostname git.np-dms.work:2222 --title "..." --body "..."`. Use a heredoc for multi-line bodies. +- **Read an issue**: `gh issue view --hostname git.np-dms.work:2222 --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --hostname git.np-dms.work:2222 --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --hostname git.np-dms.work:2222 --body "..."` +- **Apply / remove labels**: `gh issue edit --hostname git.np-dms.work:2222 --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --hostname git.np-dms.work:2222 --comment "..."` + +Infer the repo from `git remote -v` — the origin is `ssh://git@git.np-dms.work:2222/np-dms/lcbp3.git`. + +## When a skill says "publish to the issue tracker" + +Create a Gitea issue using `gh issue create --hostname git.np-dms.work:2222`. + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view --hostname git.np-dms.work:2222 --comments`. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md new file mode 100644 index 0000000..b716855 --- /dev/null +++ b/docs/agents/triage-labels.md @@ -0,0 +1,15 @@ +# Triage Labels + +The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. + +| Label in mattpocock/skills | Label in our tracker | Meaning | +| -------------------------- | -------------------- | ---------------------------------------- | +| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | +| `needs-info` | `needs-info` | Waiting on reporter for more information | +| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | +| `ready-for-human` | `ready-for-human` | Requires human implementation | +| `wontfix` | `wontfix` | Will not be actioned | + +When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. + +Edit the right-hand column to match whatever vocabulary you actually use. diff --git a/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx b/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx index 153926b..79102a3 100644 --- a/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx @@ -23,6 +23,7 @@ export default function WorkflowEditPage() { const router = useRouter(); const id = params?.id === 'new' ? null : (params?.id as string); + const [hasValidationErrors, setHasValidationErrors] = useState(false); const [workflowData, setWorkflowData] = useState>({ workflowName: '', description: '', @@ -102,7 +103,7 @@ export default function WorkflowEditPage() { -