@nklisch/pi-agile-workflow 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +28 -0
- package/CHANGELOG.md +591 -0
- package/README.md +107 -0
- package/docs/ARCHITECTURE.md +716 -0
- package/docs/MIGRATION.md +419 -0
- package/docs/PRINCIPLES.md +464 -0
- package/docs/ROADMAP.md +95 -0
- package/docs/SPEC.md +855 -0
- package/docs/VISION.md +106 -0
- package/extensions/agile-workflow.test.ts +419 -0
- package/extensions/agile-workflow.ts +467 -0
- package/hooks/hooks.json +52 -0
- package/hooks/scripts/prompt-context.py +628 -0
- package/hooks/scripts/substrate-maintainer.py +359 -0
- package/hooks/scripts/test_prompt_context.py +757 -0
- package/package.json +30 -0
- package/scripts/install-work-view.sh +165 -0
- package/scripts/tests/agent-metadata.test.sh +75 -0
- package/scripts/tests/bump-version.test.sh +552 -0
- package/scripts/tests/channel-parity.test.sh +117 -0
- package/scripts/tests/convert-content-integrity.test.sh +246 -0
- package/scripts/tests/convert-install-routing.test.sh +159 -0
- package/scripts/tests/install-work-view.test.sh +411 -0
- package/scripts/tests/pi-package-metadata.test.sh +119 -0
- package/scripts/tests/work-board-shim.test.sh +239 -0
- package/scripts/tests/work-view-dist-version.test.sh +105 -0
- package/scripts/work-board.sh +64 -0
- package/scripts/work-view.sh +423 -0
- package/skills/autopilot/SKILL.md +330 -0
- package/skills/autopilot/agents/openai.yaml +6 -0
- package/skills/board/SKILL.md +62 -0
- package/skills/board/agents/openai.yaml +6 -0
- package/skills/bold-refactor/SKILL.md +244 -0
- package/skills/bold-refactor/agents/openai.yaml +6 -0
- package/skills/bug-scan/SKILL.md +378 -0
- package/skills/bug-scan/agents/openai.yaml +6 -0
- package/skills/bug-scan/references/async-promises.md +119 -0
- package/skills/bug-scan/references/concurrency-races.md +148 -0
- package/skills/bug-scan/references/data-layer.md +167 -0
- package/skills/bug-scan/references/error-handling.md +197 -0
- package/skills/bug-scan/references/gate-item-template.md +94 -0
- package/skills/bug-scan/references/language-footguns.md +219 -0
- package/skills/bug-scan/references/parked-item-template.md +103 -0
- package/skills/bug-scan/references/report-template.md +115 -0
- package/skills/bug-scan/references/resource-leaks.md +199 -0
- package/skills/bug-scan/references/state-closures.md +148 -0
- package/skills/bug-scan/references/time-numbers.md +158 -0
- package/skills/convert/SKILL.md +1463 -0
- package/skills/convert/agents/openai.yaml +6 -0
- package/skills/convert/references/legacy-overlap-migration.md +179 -0
- package/skills/deep-code-scan/SKILL.md +378 -0
- package/skills/deep-code-scan/agents/openai.yaml +6 -0
- package/skills/deep-code-scan/references/artifact-ledger.md +148 -0
- package/skills/deep-code-scan/references/consolidation.md +91 -0
- package/skills/deep-code-scan/references/decomposition.md +127 -0
- package/skills/deep-code-scan/references/item-templates.md +263 -0
- package/skills/deep-code-scan/references/lane-catalog.md +129 -0
- package/skills/deep-code-scan/references/review-gauntlet.md +108 -0
- package/skills/deep-code-scan/references/scanner-brief.md +152 -0
- package/skills/e2e-test-design/SKILL.md +517 -0
- package/skills/e2e-test-design/references/anti-tautology.md +261 -0
- package/skills/e2e-test-design/references/service-mocks.md +234 -0
- package/skills/e2e-test-design/references/test-taxonomy.md +211 -0
- package/skills/epic-design/SKILL.md +476 -0
- package/skills/epicize/SKILL.md +210 -0
- package/skills/epicize/agents/openai.yaml +6 -0
- package/skills/feature-design/SKILL.md +472 -0
- package/skills/fix/SKILL.md +173 -0
- package/skills/gate-cruft/SKILL.md +258 -0
- package/skills/gate-docs/SKILL.md +278 -0
- package/skills/gate-patterns/SKILL.md +389 -0
- package/skills/gate-refactor/SKILL.md +408 -0
- package/skills/gate-security/SKILL.md +275 -0
- package/skills/gate-tests/SKILL.md +367 -0
- package/skills/groom/SKILL.md +162 -0
- package/skills/groom/agents/openai.yaml +6 -0
- package/skills/ideate/SKILL.md +185 -0
- package/skills/ideate/agents/openai.yaml +6 -0
- package/skills/implement/SKILL.md +288 -0
- package/skills/implement-orchestrator/SKILL.md +604 -0
- package/skills/park/SKILL.md +111 -0
- package/skills/perf-design/SKILL.md +427 -0
- package/skills/perf-scout/SKILL.md +404 -0
- package/skills/perf-scout/agents/openai.yaml +6 -0
- package/skills/perf-scout/references/algorithmic-and-data-structures.md +126 -0
- package/skills/perf-scout/references/approximation-and-precomputation.md +136 -0
- package/skills/perf-scout/references/caching-and-memoization.md +123 -0
- package/skills/perf-scout/references/compiler-runtime-and-language.md +136 -0
- package/skills/perf-scout/references/database-and-storage-internals.md +144 -0
- package/skills/perf-scout/references/distributed-systems.md +150 -0
- package/skills/perf-scout/references/game-engine-and-realtime.md +136 -0
- package/skills/perf-scout/references/gpu-and-accelerators.md +130 -0
- package/skills/perf-scout/references/idea-ranking.md +75 -0
- package/skills/perf-scout/references/io-and-batching.md +135 -0
- package/skills/perf-scout/references/memory-and-data-locality.md +137 -0
- package/skills/perf-scout/references/parallelism-and-vectorization.md +126 -0
- package/skills/perf-scout/references/parked-item-template.md +117 -0
- package/skills/perf-scout/references/peer-review-pass.md +150 -0
- package/skills/perf-scout/references/report-template.md +148 -0
- package/skills/principles/SKILL.md +678 -0
- package/skills/principles/references/models.md +191 -0
- package/skills/principles/references/subagents.md +158 -0
- package/skills/prose-author/SKILL.md +185 -0
- package/skills/refactor-conventions-creator/SKILL.md +359 -0
- package/skills/refactor-conventions-creator/agents/openai.yaml +6 -0
- package/skills/refactor-conventions-creator/references/common-structures.md +91 -0
- package/skills/refactor-conventions-creator/references/common-styles.md +90 -0
- package/skills/refactor-design/SKILL.md +384 -0
- package/skills/release-deploy/SKILL.md +667 -0
- package/skills/release-deploy/agents/openai.yaml +6 -0
- package/skills/research/SKILL.md +166 -0
- package/skills/research/agents/openai.yaml +6 -0
- package/skills/review/SKILL.md +267 -0
- package/skills/review/references/deep-review.md +98 -0
- package/skills/review/references/review-lenses.md +68 -0
- package/skills/review/references/substrate-side-effects.md +161 -0
- package/skills/review/references/target-resolution.md +52 -0
- package/skills/scope/SKILL.md +486 -0
- package/work-view/Cargo.lock +486 -0
- package/work-view/Cargo.toml +9 -0
- package/work-view/crates/cli/.work-view-version +1 -0
- package/work-view/crates/cli/Cargo.toml +18 -0
- package/work-view/crates/cli/src/actionable.rs +500 -0
- package/work-view/crates/cli/src/args.rs +792 -0
- package/work-view/crates/cli/src/board/assets/board.css +855 -0
- package/work-view/crates/cli/src/board/assets/board.js +259 -0
- package/work-view/crates/cli/src/board/assets/card.js +139 -0
- package/work-view/crates/cli/src/board/assets/components.css +290 -0
- package/work-view/crates/cli/src/board/assets/dependency.js +1193 -0
- package/work-view/crates/cli/src/board/assets/detail.js +256 -0
- package/work-view/crates/cli/src/board/assets/filters.js +389 -0
- package/work-view/crates/cli/src/board/assets/index.html +77 -0
- package/work-view/crates/cli/src/board/assets/kanban.js +231 -0
- package/work-view/crates/cli/src/board/assets/markdown.js +260 -0
- package/work-view/crates/cli/src/board/assets/motion.css +131 -0
- package/work-view/crates/cli/src/board/assets/state.js +245 -0
- package/work-view/crates/cli/src/board/assets/table.js +304 -0
- package/work-view/crates/cli/src/board/assets/tokens.css +602 -0
- package/work-view/crates/cli/src/board/assets/views.js +41 -0
- package/work-view/crates/cli/src/board/assets.rs +162 -0
- package/work-view/crates/cli/src/board/feed.rs +204 -0
- package/work-view/crates/cli/src/board/mod.rs +246 -0
- package/work-view/crates/cli/src/board/open.rs +144 -0
- package/work-view/crates/cli/src/board/server.rs +407 -0
- package/work-view/crates/cli/src/main.rs +195 -0
- package/work-view/crates/cli/src/render.rs +354 -0
- package/work-view/crates/cli/src/scope.rs +157 -0
- package/work-view/crates/cli/src/stale.rs +581 -0
- package/work-view/crates/cli/tests/board-js/dependency-table.test.mjs +64 -0
- package/work-view/crates/cli/tests/board-js/expanded-browsing.test.mjs +79 -0
- package/work-view/crates/cli/tests/board-js/harness-smoke.test.mjs +11 -0
- package/work-view/crates/cli/tests/board-js/harness.mjs +437 -0
- package/work-view/crates/cli/tests/board-js/kanban-detail.test.mjs +75 -0
- package/work-view/crates/cli/tests/board-js/markdown-filter.test.mjs +60 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/CONVENTIONS.md +3 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/epics/epic-alpha.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-a.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-b.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-alpha-1.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-research-1.md +19 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-done.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-shipped.md +18 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/backlog/idea-backlog.md +7 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/CONVENTIONS.md +6 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-dep-done.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-design-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-impl-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/stories/story-review-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/CONVENTIONS.md +4 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/epics/malformed-no-closing-fence.md +8 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/features/good-item.md +16 -0
- package/work-view/crates/cli/tests/integration.rs +2833 -0
- package/work-view/crates/core/Cargo.toml +13 -0
- package/work-view/crates/core/src/error.rs +61 -0
- package/work-view/crates/core/src/filter.rs +804 -0
- package/work-view/crates/core/src/graph.rs +324 -0
- package/work-view/crates/core/src/index.rs +590 -0
- package/work-view/crates/core/src/lib.rs +54 -0
- package/work-view/crates/core/src/model.rs +202 -0
- package/work-view/crates/core/src/parse.rs +494 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/CONVENTIONS.md +15 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/epics/epic-core.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-parser.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-query.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/malformed-no-id.md +15 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-parse-unit.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-security-gate.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-old.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-parser.md +17 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/backlog/idea-future-board.md +7 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/CONVENTIONS.md +10 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/archive/dup-item.md +18 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/releases/v2.0/dup-item.md +18 -0
- package/work-view/crates/core/tests/integration.rs +541 -0
- package/work-view/dist/.gitattributes +1 -0
- package/work-view/dist/README.md +42 -0
- package/work-view/dist/aarch64-apple-darwin/work-view +0 -0
- package/work-view/dist/aarch64-unknown-linux-musl/work-view +0 -0
- package/work-view/dist/x86_64-apple-darwin/work-view +0 -0
- package/work-view/dist/x86_64-unknown-linux-musl/work-view +0 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bug-scan
|
|
3
|
+
description: >
|
|
4
|
+
Deep multi-angle correctness bug hunt. Use when the user asks to "scan for bugs", "bug hunt", "deep
|
|
5
|
+
bug audit", "find hidden bugs", "find race conditions", or audit lurking correctness issues. Fans
|
|
6
|
+
parallel scanner agents across relevant domains such as concurrency, async, state, resource
|
|
7
|
+
leaks, time/numbers, error handling, data layer, and language footguns. Standalone mode writes a
|
|
8
|
+
scored report; gate mode binds to a release bundle and emits .work/active/stories/ items with
|
|
9
|
+
gate_origin:bugs. Distinct from gate-security, review, perf-design, and repo-eval.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Bug-Scan
|
|
13
|
+
|
|
14
|
+
You orchestrate a deep, multi-angle hunt for hard-to-find correctness bugs. You detect the
|
|
15
|
+
stack, choose relevant bug domains, dispatch **one deep scanner agent per selected domain in parallel**, and
|
|
16
|
+
either write a scored report (standalone) or produce items in `.work/active/stories/` (gate
|
|
17
|
+
mode). Use a generic sub-agent prompted with the scanner posture from `../principles/references/subagents.md`. Domain selection is the
|
|
18
|
+
scope-size gate: do not spawn a scanner for a domain just because it exists in the table. Each
|
|
19
|
+
scanner loads only its domain's reference —
|
|
20
|
+
that's the progressive-disclosure move that keeps each scanner focused and the
|
|
21
|
+
orchestrator's context lean.
|
|
22
|
+
|
|
23
|
+
Scanner strength is explicit: spawn one source-read-only scanner agent per
|
|
24
|
+
selected domain with high inspection/reviewer reasoning. Use a generic sub-agent prompted with the scanner posture from `../principles/references/subagents.md`. Use extra-high reasoning only for
|
|
25
|
+
concurrency, data-layer, or time bugs in high-risk domains, very large scopes,
|
|
26
|
+
or repeat scans that previously missed issues. These are scanners, not fixers.
|
|
27
|
+
- For the dynamic scanner prompt posture, load
|
|
28
|
+
`../principles/references/subagents.md`. If no suitable scanner subagent adapter is
|
|
29
|
+
available, use the same-host read-only analysis fallback.
|
|
30
|
+
|
|
31
|
+
This skill hunts **correctness** bugs, not vulnerabilities, not perf, not style. Use the
|
|
32
|
+
sibling skills for those.
|
|
33
|
+
|
|
34
|
+
## Modes
|
|
35
|
+
|
|
36
|
+
### Standalone (default)
|
|
37
|
+
- `/agile-workflow:bug-scan` — scan the whole repo
|
|
38
|
+
- `/agile-workflow:bug-scan <path>` — scope to a directory or glob (`src/api/`, `**/*.ts`)
|
|
39
|
+
- `/agile-workflow:bug-scan --no-park` — write report only, skip backlog parking (any mode)
|
|
40
|
+
|
|
41
|
+
Output:
|
|
42
|
+
1. `bug-scan-report.md` at the repo root — domain scores, severity-grouped findings,
|
|
43
|
+
remediation directions, list of parked item ids.
|
|
44
|
+
2. **One parked backlog item per finding** at `.work/backlog/bug-scan-<slug>.md` — minimal
|
|
45
|
+
frontmatter (`bug_origin: scan`, `bug_severity`, `bug_domain`, `bug_location`) so findings
|
|
46
|
+
are tracked in the substrate alongside the report. The user can elevate any of them later
|
|
47
|
+
via `/agile-workflow:scope`. Idempotent on re-runs (existing findings at the same
|
|
48
|
+
`file:line` are skipped). Suppress with `--no-park`.
|
|
49
|
+
|
|
50
|
+
> Why not write standalone Criticals straight to `.work/active/stories/`? Standalone is
|
|
51
|
+
> exploratory — the user invoked it to *see* what's there, not commit to fixing it. For
|
|
52
|
+
> release-bound, must-fix-before-ship Criticals, use **gate mode**, which writes to active.
|
|
53
|
+
|
|
54
|
+
### Gate mode
|
|
55
|
+
- `/agile-workflow:bug-scan --release <version>` — scan only files touched by items bound
|
|
56
|
+
to that release
|
|
57
|
+
- Auto-invoked by `/agile-workflow:release-deploy` during the quality-gate stage
|
|
58
|
+
|
|
59
|
+
Output: items in `.work/active/stories/` (Critical/High → `stage: implementing`,
|
|
60
|
+
Medium → `stage: drafting`, Low → backlog), each with `gate_origin: bugs`.
|
|
61
|
+
|
|
62
|
+
The release ships when all bound items (including these) reach `stage: done` — same as
|
|
63
|
+
gate-security.
|
|
64
|
+
|
|
65
|
+
## Bug domains (and their references)
|
|
66
|
+
|
|
67
|
+
The scanner agents each load exactly one of these. Each reference contains ~15-25 patterns
|
|
68
|
+
with grep-style detection signals, "why hard to find" notes, examples, and fix directions.
|
|
69
|
+
|
|
70
|
+
| Domain | Reference | When to load |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| Concurrency & races | [references/concurrency-races.md](references/concurrency-races.md) | Code uses threads, processes, goroutines, async runtimes, shared mutable state, locks, channels, atomics |
|
|
73
|
+
| Async / promises | [references/async-promises.md](references/async-promises.md) | Code uses async/await, Promise/Future/Task, coroutines, event loops, non-blocking I/O |
|
|
74
|
+
| State & closures | [references/state-closures.md](references/state-closures.md) | Code uses closures over loop vars, framework reactivity (React/Vue/Svelte/Solid), stores (Redux/Zustand/Pinia), TanStack Query |
|
|
75
|
+
| Resource leaks | [references/resource-leaks.md](references/resource-leaks.md) | Code opens files/sockets/connections, subscribes, registers listeners, schedules timers, spawns threads/processes, acquires locks |
|
|
76
|
+
| Time & numbers | [references/time-numbers.md](references/time-numbers.md) | Code handles dates/timestamps/durations, money, percentages, float comparisons, integer overflow risk |
|
|
77
|
+
| Error handling | [references/error-handling.md](references/error-handling.md) | Code throws/catches/raises, returns Result/Either, uses defer/finally/with, calls fallible operations in sequence |
|
|
78
|
+
| Data layer | [references/data-layer.md](references/data-layer.md) | Code touches a database, ORM, message queue, external service, or coordinates across processes |
|
|
79
|
+
| Language footguns | [references/language-footguns.md](references/language-footguns.md) | Always relevant — the scanner reads only the section matching the detected language |
|
|
80
|
+
|
|
81
|
+
## Phase 1: Stack discovery
|
|
82
|
+
|
|
83
|
+
Detect what you're scanning:
|
|
84
|
+
- **Languages** — file extensions, manifests (`package.json`, `tsconfig.json`, `pyproject.toml`,
|
|
85
|
+
`Cargo.toml`, `go.mod`, `pom.xml`, `build.gradle`, `*.csproj`, `Gemfile`).
|
|
86
|
+
- **Frameworks** — React, Vue, Svelte, Next, Nuxt, Django, FastAPI, Rails, Spring, Actix, Axum,
|
|
87
|
+
Express, NestJS, Tokio, asyncio.
|
|
88
|
+
- **Data layer** — Prisma, Drizzle, SQLAlchemy, Django ORM, ActiveRecord, Mongoose, Redis,
|
|
89
|
+
Kafka, NATS, SQS.
|
|
90
|
+
- **Concurrency primitives** — goroutines, threads, asyncio.create_task, tokio::spawn, workers.
|
|
91
|
+
- **Entry points** — API routes, CLI mains, background workers, cron, webhook handlers.
|
|
92
|
+
|
|
93
|
+
Summarize the stack in 4-6 lines.
|
|
94
|
+
|
|
95
|
+
## Phase 2: Domain selection
|
|
96
|
+
|
|
97
|
+
Map detected stack features to the 8 domains. Mark each as **most relevant**, **relevant**,
|
|
98
|
+
or **skip** based on what's in the code and the concrete scan scope. For a small
|
|
99
|
+
path with no async, no data layer, and no external resources, skip those domains
|
|
100
|
+
instead of launching empty scanners. For a broad release bundle or repo-wide
|
|
101
|
+
audit, include every domain with real evidence in Phase 1.
|
|
102
|
+
|
|
103
|
+
### Standalone mode
|
|
104
|
+
**structured question tool checkpoint** (multi-select): show the 8 domains with relevance annotations,
|
|
105
|
+
recommend 4-6 for a focused scan or all 8 for a full audit. Default to all "most relevant" +
|
|
106
|
+
"relevant" if user doesn't choose.
|
|
107
|
+
|
|
108
|
+
### Gate mode
|
|
109
|
+
Skip the prompt. Default to all domains that touch any file in the bundle.
|
|
110
|
+
|
|
111
|
+
## Phase 3: Fan-out scan
|
|
112
|
+
|
|
113
|
+
For each selected domain, spawn **one parallel scanner agent in a single
|
|
114
|
+
message** so they run concurrently.
|
|
115
|
+
|
|
116
|
+
Use a generic sub-agent prompted with the scanner posture from `../principles/references/subagents.md` with high
|
|
117
|
+
inspection/reviewer reasoning; escalate to extra-high only for high-risk
|
|
118
|
+
domains, very large scopes, or repeat scans that previously missed issues. If a
|
|
119
|
+
host cannot spawn the scanner role, use the same-host read-only analysis
|
|
120
|
+
fallback.
|
|
121
|
+
|
|
122
|
+
### Scope (passed into every scanner)
|
|
123
|
+
|
|
124
|
+
- **Standalone**: the user's path arg, or the whole repo if no arg. Resolve to a concrete file
|
|
125
|
+
list with `git ls-files` (and the path filter, if any).
|
|
126
|
+
- **Gate mode**: only files touched by items bound to the release. `--release`
|
|
127
|
+
auto-widens to ALL tiers (active + archive + releases). Include late-bound
|
|
128
|
+
archived stubs — their bodies may be pruned, but the item id still recovers
|
|
129
|
+
the bundle commits/files. Ignore only the release orchestration item
|
|
130
|
+
(`kind: release`).
|
|
131
|
+
```bash
|
|
132
|
+
.work/bin/work-view --release <version> --paths | while IFS= read -r item; do
|
|
133
|
+
kind=$(grep -m1 '^kind:' "$item" | awk '{print $2}')
|
|
134
|
+
[ "$kind" = "release" ] && continue
|
|
135
|
+
id=$(grep -m1 '^id:' "$item" | awk '{print $2}')
|
|
136
|
+
git log --grep "$id" --format='%H' | xargs -I{} git diff-tree --no-commit-id --name-only -r {}
|
|
137
|
+
done | sort -u > /tmp/bundle-files-<version>.txt
|
|
138
|
+
```
|
|
139
|
+
Capture already-tracked findings to feed back as "skip these":
|
|
140
|
+
```bash
|
|
141
|
+
.work/bin/work-view --release <version> --gate bugs --paths
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Scanner brief template
|
|
145
|
+
|
|
146
|
+
Each scanner gets:
|
|
147
|
+
|
|
148
|
+
> You are a bug scanner agent for the **<domain>** domain. Use the
|
|
149
|
+
> agile-workflow scanner contract: source-read-only, no fixes, no recursive
|
|
150
|
+
> sub-agents.
|
|
151
|
+
>
|
|
152
|
+
> **Reference (load FIRST)**: `<absolute path to references/<domain>.md>`
|
|
153
|
+
> Read the whole file. It contains the patterns to hunt for in this domain.
|
|
154
|
+
>
|
|
155
|
+
> **Scope** — scan ONLY these files:
|
|
156
|
+
> ```
|
|
157
|
+
> <file list>
|
|
158
|
+
> ```
|
|
159
|
+
>
|
|
160
|
+
> **Stack profile** (from orchestrator):
|
|
161
|
+
> ```
|
|
162
|
+
> <stack summary>
|
|
163
|
+
> ```
|
|
164
|
+
>
|
|
165
|
+
> **Already-tracked findings to skip** (gate mode only):
|
|
166
|
+
> ```
|
|
167
|
+
> <file:line / pattern pairs, or "none">
|
|
168
|
+
> ```
|
|
169
|
+
>
|
|
170
|
+
> **Method**:
|
|
171
|
+
> 1. Load the domain reference. Note the patterns and detection signals.
|
|
172
|
+
> 2. Web-search 1-3 times for current pitfalls in this domain for the specific stack
|
|
173
|
+
> (e.g. "React 19 stale closure useEffect 2025", "Go 1.22 loop variable gotchas").
|
|
174
|
+
> 3. Apply the reference's grep heuristics to the scope. Read flagged files. Confirm matches
|
|
175
|
+
> by reading the code — don't report on grep hits alone.
|
|
176
|
+
> 4. For each confirmed bug, record:
|
|
177
|
+
> - **Title** (one line)
|
|
178
|
+
> - **Pattern** (which named pattern from the reference; or "new" if not in the reference)
|
|
179
|
+
> - **Severity** — see rubric below
|
|
180
|
+
> - **Location** (`file:line`)
|
|
181
|
+
> - **Evidence** (1-5 lines of the offending code, in a fenced block)
|
|
182
|
+
> - **Why it's a bug** (1-2 sentences — the specific failure mode)
|
|
183
|
+
> - **Remediation direction** (a direction, not a finished fix)
|
|
184
|
+
>
|
|
185
|
+
> **Severity rubric** — every finding gets one of:
|
|
186
|
+
> | Severity | Meaning |
|
|
187
|
+
> |---|---|
|
|
188
|
+
> | Critical | Will cause data loss, corruption, hang, or wrong financial outcome under realistic load. Must fix before shipping. |
|
|
189
|
+
> | High | Will cause incorrect behavior or crash under uncommon-but-real conditions (specific input, race window, error path). Must fix before shipping. |
|
|
190
|
+
> | Medium | Real bug, hard to trigger or limited blast radius. Should fix; can defer with explicit acknowledgement. |
|
|
191
|
+
> | Low | Edge case, latent bug, defensive-only. Backlog. |
|
|
192
|
+
> | Info | Not a finding. Don't return Info entries. |
|
|
193
|
+
>
|
|
194
|
+
> **Rules**:
|
|
195
|
+
> - Cite file:line for every finding.
|
|
196
|
+
> - Don't report a pattern unless you read the surrounding code and confirmed the bug applies
|
|
197
|
+
> here. Grep hits alone are not findings.
|
|
198
|
+
> - Don't fabricate. Empty findings list is a valid answer.
|
|
199
|
+
> - Don't implement fixes. Findings only.
|
|
200
|
+
> - Skip findings matching the already-tracked list.
|
|
201
|
+
>
|
|
202
|
+
> **Output format** — return a single markdown document with:
|
|
203
|
+
> ```
|
|
204
|
+
> ## Domain: <name>
|
|
205
|
+
> ## Stack notes: <one-line> what's relevant
|
|
206
|
+
> ## Findings
|
|
207
|
+
>
|
|
208
|
+
> ### Finding 1
|
|
209
|
+
> - **Title**: ...
|
|
210
|
+
> - **Pattern**: ...
|
|
211
|
+
> - **Severity**: Critical | High | Medium | Low
|
|
212
|
+
> - **Location**: `file:line`
|
|
213
|
+
> - **Evidence**:
|
|
214
|
+
> ```<lang>
|
|
215
|
+
> <code>
|
|
216
|
+
> ```
|
|
217
|
+
> - **Why it's a bug**: ...
|
|
218
|
+
> - **Remediation direction**: ...
|
|
219
|
+
> ```
|
|
220
|
+
> Followed by:
|
|
221
|
+
> ```
|
|
222
|
+
> ## Summary
|
|
223
|
+
> - Files scanned: <n>
|
|
224
|
+
> - Patterns applied: <list>
|
|
225
|
+
> - Findings: Critical=<n>, High=<n>, Medium=<n>, Low=<n>
|
|
226
|
+
> ```
|
|
227
|
+
|
|
228
|
+
### Wave coordination
|
|
229
|
+
|
|
230
|
+
- Dispatch all selected scanners in a **single message** (parallel).
|
|
231
|
+
- If a scanner returns an error, record it as a gap in the report — do not re-run blindly.
|
|
232
|
+
- If a scanner returns >25 findings, ask it (via follow-up message) to keep only the top 25 by
|
|
233
|
+
severity (anything beyond suggests pattern over-matching).
|
|
234
|
+
|
|
235
|
+
## Phase 4: Aggregate & dedupe
|
|
236
|
+
|
|
237
|
+
Collect every scanner's findings. Then:
|
|
238
|
+
|
|
239
|
+
1. **Dedupe** — if the same `file:line` appears in two domains, keep the higher-severity entry
|
|
240
|
+
and add a "also flagged by: <domain>" note.
|
|
241
|
+
2. **Cluster** — group findings by file. A file with 5+ findings probably needs a single
|
|
242
|
+
broader "this module needs rework" item rather than 5 small items.
|
|
243
|
+
3. **Sort** — by severity (Critical → High → Medium → Low), then by file.
|
|
244
|
+
4. **Score each domain** 0-10 using the rubric below.
|
|
245
|
+
|
|
246
|
+
### Domain scoring rubric
|
|
247
|
+
|
|
248
|
+
| Score | Meaning |
|
|
249
|
+
|---|---|
|
|
250
|
+
| 0-1 | Critical findings present, multiple. Codebase is unsafe in this domain. |
|
|
251
|
+
| 2-3 | One Critical or several High. Major work needed. |
|
|
252
|
+
| 4-5 | Several Medium findings; one Critical/High at most. Hygiene gaps. |
|
|
253
|
+
| 6-7 | Clean — only Low findings or one Medium. Standard quality. |
|
|
254
|
+
| 8-9 | No findings + the code shows proactive defensive patterns (transaction boundaries,
|
|
255
|
+
AbortControllers, idempotency keys, explicit timeouts, etc.). |
|
|
256
|
+
| 10 | Zero findings + comprehensive defensive design across the domain. |
|
|
257
|
+
|
|
258
|
+
Floor: a Critical finding caps the domain score at 3. Multiple High caps at 5.
|
|
259
|
+
|
|
260
|
+
Compute an overall score: weighted average where "most relevant" domains weigh 2x, "relevant"
|
|
261
|
+
weigh 1x. Round to one decimal.
|
|
262
|
+
|
|
263
|
+
## Phase 5: Output
|
|
264
|
+
|
|
265
|
+
### Standalone mode
|
|
266
|
+
|
|
267
|
+
Two outputs: a markdown report AND parked backlog items (unless `--no-park`).
|
|
268
|
+
|
|
269
|
+
#### 5a. Park findings to backlog (default)
|
|
270
|
+
|
|
271
|
+
Skip this step entirely if invoked with `--no-park`. Otherwise:
|
|
272
|
+
|
|
273
|
+
1. **Verify substrate**. If `.work/CONVENTIONS.md` is missing, the project isn't on
|
|
274
|
+
agile-workflow yet — skip parking, note it in the report, and tell the user to run
|
|
275
|
+
`/agile-workflow:convert` if they want substrate tracking.
|
|
276
|
+
|
|
277
|
+
2. **Build the existing-findings index** for idempotency:
|
|
278
|
+
```bash
|
|
279
|
+
mkdir -p /tmp
|
|
280
|
+
: > /tmp/bugscan-existing.txt
|
|
281
|
+
for f in .work/backlog/bug-scan-*.md; do
|
|
282
|
+
[ -f "$f" ] || continue
|
|
283
|
+
grep -q '^bug_origin: scan$' "$f" || continue
|
|
284
|
+
loc=$(grep -m1 '^bug_location:' "$f" | awk '{print $2}')
|
|
285
|
+
[ -n "$loc" ] && echo "$loc $f" >> /tmp/bugscan-existing.txt
|
|
286
|
+
done
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
3. **Park each finding** as one file at `.work/backlog/bug-scan-<slug>.md` using the template
|
|
290
|
+
at [references/parked-item-template.md](references/parked-item-template.md). Skip any
|
|
291
|
+
finding whose `file:line` already appears in `/tmp/bugscan-existing.txt` and tally those
|
|
292
|
+
as "duplicates skipped". On slug collision, suffix `-2`, `-3`.
|
|
293
|
+
|
|
294
|
+
4. **Commit** all parked files in one commit:
|
|
295
|
+
```bash
|
|
296
|
+
git add .work/backlog/bug-scan-*.md
|
|
297
|
+
git commit -m "bug-scan: park <N> findings (<scope>)"
|
|
298
|
+
```
|
|
299
|
+
If working tree was dirty before parking, commit only the new backlog files (`git add`
|
|
300
|
+
with explicit paths — never `git add -A`).
|
|
301
|
+
|
|
302
|
+
#### 5b. Write the report
|
|
303
|
+
|
|
304
|
+
Write to `bug-scan-report.md` in the repo root using the template at
|
|
305
|
+
[references/report-template.md](references/report-template.md). Include:
|
|
306
|
+
|
|
307
|
+
- Overall score (X.X/10)
|
|
308
|
+
- Per-domain score table
|
|
309
|
+
- Findings grouped by severity, each with its parked item id (or "not parked: --no-park"
|
|
310
|
+
or "not parked: no substrate")
|
|
311
|
+
- "Top 3" callout for the most-critical findings
|
|
312
|
+
- Domains skipped + why
|
|
313
|
+
- Park summary: parked count, duplicates skipped, opt-outs
|
|
314
|
+
|
|
315
|
+
#### 5c. Summarize to the user
|
|
316
|
+
|
|
317
|
+
- Overall score
|
|
318
|
+
- Severity counts (Critical / High / Medium / Low)
|
|
319
|
+
- Top 3 critical findings (one line each, with `file:line`)
|
|
320
|
+
- Path to the report
|
|
321
|
+
- Parked count + duplicates skipped (or "parking skipped (--no-park)" / "parking skipped (no substrate)")
|
|
322
|
+
- **structured question tool** (four options):
|
|
323
|
+
- "Elevate criticals to `stage:implementing` via `/agile-workflow:scope`"
|
|
324
|
+
- "Hand top finding to `/agile-workflow:fix` now"
|
|
325
|
+
- "Dive deeper into a specific domain"
|
|
326
|
+
- "Done for now — leave the parked items in backlog"
|
|
327
|
+
|
|
328
|
+
### Gate mode
|
|
329
|
+
|
|
330
|
+
For each finding above Info, create an item file in `.work/active/stories/` (or backlog for
|
|
331
|
+
Low) using the template at [references/gate-item-template.md](references/gate-item-template.md).
|
|
332
|
+
|
|
333
|
+
Severity → stage mapping:
|
|
334
|
+
- **Critical** / **High** → `stage: implementing`
|
|
335
|
+
- **Medium** → `stage: drafting`
|
|
336
|
+
- **Low** → backlog file at `.work/backlog/`
|
|
337
|
+
|
|
338
|
+
ID convention: `gate-bugs-<short-slug>` where slug describes the finding (e.g.
|
|
339
|
+
`gate-bugs-stale-closure-in-cart-effect`).
|
|
340
|
+
|
|
341
|
+
Commit:
|
|
342
|
+
```bash
|
|
343
|
+
git add .work/active/stories/ .work/backlog/
|
|
344
|
+
git commit -m "gate-bugs: <N> findings for <version>"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Then report to the user (same format as gate-security):
|
|
348
|
+
- Bundle version
|
|
349
|
+
- Findings by severity
|
|
350
|
+
- Items created (count + new ids)
|
|
351
|
+
- Already-tracked duplicates skipped
|
|
352
|
+
- Next step (release ships when bound items reach `stage: done`)
|
|
353
|
+
|
|
354
|
+
## Guardrails
|
|
355
|
+
|
|
356
|
+
- **The scanning happens in the scanner agents, not here.** Your job is stack discovery,
|
|
357
|
+
scanner dispatch, and result aggregation. Do not re-do a scanner's work in the
|
|
358
|
+
orchestrator's context — that throws away the progressive-disclosure win.
|
|
359
|
+
- **Fan out selected domains.** Once Phase 2 has selected domains, run one
|
|
360
|
+
scanner per selected domain in parallel. Do not serialize them, and do not
|
|
361
|
+
spawn skipped domains.
|
|
362
|
+
- **Always cite file:line.** Findings without locations are not findings.
|
|
363
|
+
- **Don't fabricate.** A scanner returning zero findings for a domain is a valid result. Score
|
|
364
|
+
it honestly (7, not 10 — absence ≠ active hardening).
|
|
365
|
+
- **Confirm grep hits.** Detection signals are starting points, not findings. Every reported
|
|
366
|
+
bug must have been read and understood in context.
|
|
367
|
+
- **Respect scope.** If the user gave a path, don't expand. If gate mode, scan only the bundle.
|
|
368
|
+
- **No fixes.** This skill produces findings/items only. Remediation goes through
|
|
369
|
+
`/agile-workflow:fix` or `/agile-workflow:implement`.
|
|
370
|
+
- **Idempotent gate re-runs.** Pass already-tracked findings into scanner briefs so they skip
|
|
371
|
+
duplicates; double-check on item-write before creating files.
|
|
372
|
+
- **Idempotent standalone re-runs.** Before parking, scan `.work/backlog/bug-scan-*.md` for
|
|
373
|
+
existing `bug_origin: scan` items, build a `file:line → id` index, and skip findings already
|
|
374
|
+
present. Tally and report the skip count. This keeps repeated `/agile-workflow:bug-scan`
|
|
375
|
+
invocations from flooding backlog with the same items.
|
|
376
|
+
- **Standalone parks to backlog, never to active.** Even Critical findings in standalone mode
|
|
377
|
+
go to `.work/backlog/` — the user explicitly elevates them via `/agile-workflow:scope` if
|
|
378
|
+
they choose. The path to active is gate mode (release-bound, intentional).
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Async / Promise / Coroutine — Bug Reference
|
|
2
|
+
|
|
3
|
+
> **When to load this reference**: when scanning code with async/await, Promise/Future/Task,
|
|
4
|
+
> coroutines, event loops, or non-blocking I/O. Goroutines/channels live in concurrency-races.md.
|
|
5
|
+
|
|
6
|
+
## Detection signals
|
|
7
|
+
- JS/TS: `async function`, `\bawait\b`, `Promise\.(all|allSettled|race|any)`, `\.then\(`, `\.catch\(`,
|
|
8
|
+
`setTimeout`, `setInterval`, `new Promise\(`, `\.forEach\(.*async`, `async \(.*\) =>`, top-level `await`.
|
|
9
|
+
- Python: `async def`, `\bawait\b`, `asyncio\.(gather|create_task|run|wait|shield)`, `trio\.open_nursery`,
|
|
10
|
+
`anyio\.`, `loop\.run_in_executor`, `time\.sleep` inside `async def`, `requests\.` inside `async def`.
|
|
11
|
+
- Rust: `\.await`, `tokio::spawn`, `tokio::select!`, `async fn`, `async move`, `block_on`,
|
|
12
|
+
`futures::join!`, `try_join!`, `std::thread::sleep` inside async, `std::sync::Mutex` near `.await`.
|
|
13
|
+
- Swift: `Task \{`, `await `, `actor `, `MainActor`, `withCheckedContinuation`, `Task.detached`.
|
|
14
|
+
- C#: `async Task`, `async void`, `\.Result\b`, `\.Wait\(`, `\.GetAwaiter\(\)\.GetResult`, `ConfigureAwait`.
|
|
15
|
+
- Kotlin: `suspend fun`, `launch \{`, `async \{`, `GlobalScope`, `runBlocking`, `withContext`,
|
|
16
|
+
`SupervisorJob`, `suspendCoroutine` (vs Cancellable).
|
|
17
|
+
|
|
18
|
+
## Patterns
|
|
19
|
+
|
|
20
|
+
### 1. Unhandled promise rejection / swallowed error
|
|
21
|
+
- **Signature**: `.then(…)` chain with no `.catch`; `async` function called without `await` or `.catch`; rejected promise stored to variable and ignored.
|
|
22
|
+
- **Why hard to find**: Node logs "UnhandledPromiseRejection" but does not crash by default in browsers / many runtimes. Failure path is the absence of a log.
|
|
23
|
+
- **Where to look**: top-level handlers, event listeners that call async fns, library boundaries.
|
|
24
|
+
- **Example**: `el.addEventListener('click', async () => doStuff())` — rejection is lost.
|
|
25
|
+
- **Fix direction**: wrap with `try/catch` or attach `.catch`; install global `unhandledrejection` handler.
|
|
26
|
+
- **Language variants**: Python `Task` exception lost unless `await`ed or accessed via `.exception()`; Rust `JoinHandle` errors silent until `.await`; C# `async void` throws to `SynchronizationContext` → process crash.
|
|
27
|
+
|
|
28
|
+
### 2. Fire-and-forget task lost to the void
|
|
29
|
+
- **Signature**: `asyncio.create_task(f())` / `tokio::spawn(f())` / `Task { … }` with no handle retained; no timeout, no cancellation source, no error surface.
|
|
30
|
+
- **Why hard to find**: works fine until load; under pressure tasks accumulate, GC may collect Python tasks (warning only), Rust task leaks live forever.
|
|
31
|
+
- **Where to look**: background workers, "send notification later" code, telemetry flushes.
|
|
32
|
+
- **Example**: `asyncio.create_task(send_email(u))` inside a request handler.
|
|
33
|
+
- **Fix direction**: hold reference (`self._tasks.add(t); t.add_done_callback(self._tasks.discard)`), bound queue, attach timeout + error log.
|
|
34
|
+
|
|
35
|
+
### 3. Serial `await` in a loop instead of `Promise.all`
|
|
36
|
+
- **Signature**: `for (const x of xs) { await f(x); }` when each call is independent.
|
|
37
|
+
- **Why hard to find**: correct results, just N× slower; only shows up in latency metrics.
|
|
38
|
+
- **Where to look**: hot paths, list-fetching code, migration scripts.
|
|
39
|
+
- **Example**: `for (const id of ids) results.push(await fetch(id))`.
|
|
40
|
+
- **Fix direction**: `Promise.all(ids.map(fetch))`; bound concurrency with `p-limit` / semaphore if N large.
|
|
41
|
+
- **Language variants**: Python `asyncio.gather`, Rust `futures::join_all` / `buffer_unordered`, Kotlin `awaitAll`.
|
|
42
|
+
|
|
43
|
+
### 4. `forEach` with async callback (promises ignored)
|
|
44
|
+
- **Signature**: `arr.forEach(async x => await f(x))` — `forEach` ignores returned promises.
|
|
45
|
+
- **Why hard to find**: outer function "completes" before any work finishes; downstream sees empty/partial state with no error.
|
|
46
|
+
- **Where to look**: refactors from sync to async, `.forEach` over fetches.
|
|
47
|
+
- **Fix direction**: use `for…of` with `await`, or `Promise.all(arr.map(f))`. ESLint `no-misleading-character-class` / `no-floating-promises` (TS).
|
|
48
|
+
|
|
49
|
+
### 5. Missing `await` on an async call
|
|
50
|
+
- **Signature**: `const x = fetchUser()` where `fetchUser` returns a Promise; `if (asyncCheck())` — truthy because Promise is truthy.
|
|
51
|
+
- **Why hard to find**: silent in JS without TS-strict / `@typescript-eslint/no-floating-promises`; logs show `[object Promise]` or `<Future pending>`.
|
|
52
|
+
- **Where to look**: conditionals, comparisons, JSON serialization, assignment.
|
|
53
|
+
- **Fix direction**: enable `no-floating-promises`, type the return; Python `RuntimeWarning: coroutine was never awaited` — promote to error in CI.
|
|
54
|
+
|
|
55
|
+
### 6. `Promise.all` rejecting where partial success was intended
|
|
56
|
+
- **Signature**: `Promise.all([…])` over independent best-effort work.
|
|
57
|
+
- **Why hard to find**: passes happy-path tests; under a single transient failure the whole batch is reported as failed.
|
|
58
|
+
- **Where to look**: dashboard widgets, fan-out reads, notification dispatch.
|
|
59
|
+
- **Fix direction**: `Promise.allSettled` + per-result handling. Python: `asyncio.gather(..., return_exceptions=True)`.
|
|
60
|
+
|
|
61
|
+
### 7. Race in user-input handler (stale-overwriting-fresh)
|
|
62
|
+
- **Signature**: handler kicks off `await fetch(query)`; later response sets state, but a newer keystroke has already completed.
|
|
63
|
+
- **Why hard to find**: depends on network jitter; results may look right most of the time, then flicker stale.
|
|
64
|
+
- **Where to look**: search-as-you-type, autocomplete, tab switchers, dependent dropdowns.
|
|
65
|
+
- **Fix direction**: capture request id / `AbortController`; on resolve check `if (id !== currentId) return`; in React, cleanup in `useEffect`.
|
|
66
|
+
|
|
67
|
+
### 8. Cancellation not propagated
|
|
68
|
+
- **Signature**: long-running task with no `AbortSignal` / `CancellationToken` / cooperative `ensure_active()` / `select!` on cancel branch.
|
|
69
|
+
- **Why hard to find**: caller moves on (closed tab, dropped future), worker keeps running, holding locks/DB conns.
|
|
70
|
+
- **Where to look**: streaming endpoints, retry loops, polling intervals, Tokio MPSC `send` (NOT cancel-safe — message lost on drop).
|
|
71
|
+
- **Fix direction**: thread `AbortSignal` through; Python: catch `asyncio.CancelledError` and re-raise after cleanup; Rust: prefer cancel-safe primitives or `tokio::select!` with explicit branches.
|
|
72
|
+
|
|
73
|
+
### 9. Blocking work inside an async function
|
|
74
|
+
- **Signature**: `time.sleep` / `requests.get` / sync `fs.readFileSync` / `std::thread::sleep` / heavy CPU loop inside `async def` / `async fn`.
|
|
75
|
+
- **Why hard to find**: single-threaded event loop stalls; *all* concurrent tasks pause; symptoms look like network slowness.
|
|
76
|
+
- **Where to look**: legacy lib calls in async handlers, JSON parse of huge payloads, bcrypt/argon2 calls, image processing.
|
|
77
|
+
- **Fix direction**: Python `await asyncio.to_thread(f)` or `run_in_executor`; Node worker thread / `setImmediate`; Rust `tokio::task::spawn_blocking`; C# `Task.Run` for CPU.
|
|
78
|
+
|
|
79
|
+
### 10. Holding a non-async lock across `await`
|
|
80
|
+
- **Signature**: `threading.Lock` / `std::sync::Mutex` acquired, then `.await` while held.
|
|
81
|
+
- **Why hard to find**: works under low contention; deadlocks/serializes under load; Rust will compile fine but break `Send` for tokio.
|
|
82
|
+
- **Where to look**: caches with locks, singletons, connection pools.
|
|
83
|
+
- **Fix direction**: use `asyncio.Lock` / `tokio::sync::Mutex` / Kotlin `Mutex`; or drop guard before `await`.
|
|
84
|
+
|
|
85
|
+
### 11. C# blocking on async (`.Result` / `.Wait()`)
|
|
86
|
+
- **Signature**: `someAsync().Result`, `.Wait()`, `.GetAwaiter().GetResult()` from a synchronization-context thread (UI / classic ASP.NET).
|
|
87
|
+
- **Why hard to find**: classic deadlock — continuation needs the captured context, thread is blocked waiting.
|
|
88
|
+
- **Where to look**: legacy callers of async libs, constructors, property getters.
|
|
89
|
+
- **Fix direction**: async-all-the-way; library code uses `ConfigureAwait(false)`; never `async void` except event handlers.
|
|
90
|
+
|
|
91
|
+
### 12. Swift actor reentrancy assumption
|
|
92
|
+
- **Signature**: actor method reads state, `await`s something, then writes state assuming nothing changed.
|
|
93
|
+
- **Why hard to find**: actor protects against data races, NOT against state mutation across suspension; tests that don't interleave miss it.
|
|
94
|
+
- **Where to look**: balance/quota checks, cache fill ("check-then-load"), counters.
|
|
95
|
+
- **Fix direction**: re-verify invariants after every `await`; pre-store and reuse the in-flight `Task` to dedupe; move state writes before `await`.
|
|
96
|
+
|
|
97
|
+
### 13. Kotlin: `GlobalScope` / broken structured concurrency
|
|
98
|
+
- **Signature**: `GlobalScope.launch`; manual `Job()` passed to child; `withContext(SupervisorJob())`; `try/catch` swallowing `CancellationException`.
|
|
99
|
+
- **Why hard to find**: parent cancels, child keeps running; or cancellation no longer propagates; zombie coroutines on Android leak ViewModel scopes.
|
|
100
|
+
- **Where to look**: Android ViewModels, repository singletons, retry loops that catch `Throwable`.
|
|
101
|
+
- **Fix direction**: scope to lifecycle (`viewModelScope`); rethrow `CancellationException`; avoid `suspendCoroutine` — prefer `suspendCancellableCoroutine`.
|
|
102
|
+
|
|
103
|
+
### 14. React/Vue/Svelte: state update after unmount
|
|
104
|
+
- **Signature**: `useEffect(() => { fetch().then(setX) }, [])` — no cleanup; component unmounts before resolve.
|
|
105
|
+
- **Why hard to find**: warning appears intermittently; in production silently leaks closures + may overwrite newer view state.
|
|
106
|
+
- **Where to look**: data-loading effects, subscriptions, intervals.
|
|
107
|
+
- **Fix direction**: `AbortController` in cleanup, or `let cancelled = false; return () => { cancelled = true }`; Svelte: use `onDestroy` / `$effect` cleanup; Vue: `onUnmounted`.
|
|
108
|
+
|
|
109
|
+
### 15. `Promise.race` leaving losers running
|
|
110
|
+
- **Signature**: `Promise.race([fetch(a), fetch(b)])` used as timeout; loser request keeps running, may resolve with side effects.
|
|
111
|
+
- **Why hard to find**: works functionally; leaks sockets, file handles, may trigger duplicate side effects (writes, analytics).
|
|
112
|
+
- **Where to look**: ad-hoc timeouts, "first available source" patterns.
|
|
113
|
+
- **Fix direction**: pass `AbortSignal` to the losers (`AbortSignal.any`); prefer `Promise.any` for first-success semantics; cancel explicitly.
|
|
114
|
+
|
|
115
|
+
### 16. `await` in a `finally` masking the original error
|
|
116
|
+
- **Signature**: `try { … } finally { await cleanup() }` where `cleanup` throws.
|
|
117
|
+
- **Why hard to find**: original error vanishes, replaced by cleanup error with unrelated stack.
|
|
118
|
+
- **Where to look**: transaction rollbacks, file close, lock release.
|
|
119
|
+
- **Fix direction**: catch inside `finally`, log+swallow cleanup error, let original propagate; or use `AggregateError`.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Concurrency & Race Conditions — Bug Reference
|
|
2
|
+
|
|
3
|
+
> **When to load this reference**: when scanning code that uses threads, processes, goroutines,
|
|
4
|
+
> async runtimes, shared mutable state, locks, channels, atomics, or any inter-thread/inter-task
|
|
5
|
+
> communication.
|
|
6
|
+
|
|
7
|
+
## Detection signals (where to start)
|
|
8
|
+
- Synchronization primitives: `sync.Mutex`, `sync.RWMutex`, `Lock(`, `RLock`, `synchronized`, `Arc<Mutex`, `parking_lot`, `std::mutex`, `pthread_`, `NSLock`, `DispatchQueue`.
|
|
9
|
+
- Task spawn / scheduling: `go func`, `tokio::spawn`, `thread::spawn`, `Thread(`, `threading.`, `multiprocessing.`, `asyncio.gather`, `asyncio.create_task`, `Promise.all`, `Promise.race`, `new Worker(`, `worker_threads`, `ExecutorService`, `CompletableFuture`.
|
|
10
|
+
- Channels / queues: `make(chan`, `<-`, `select {`, `mpsc::`, `crossbeam`, `queue.Queue`, `BlockingQueue`.
|
|
11
|
+
- Atomics / memory: `atomic.`, `AtomicInteger`, `AtomicReference`, `volatile`, `static mut`, `memory_order_`, `Ordering::Relaxed`, `unsafe`.
|
|
12
|
+
- Shared state markers: `Send`, `Sync`, `@GuardedBy`, `@ThreadSafe`, `dispatch_once`, `__thread`, module-level mutables.
|
|
13
|
+
- Hot files: caches, session managers, connection pools, rate limiters, schedulers, leader-election code, file-write helpers.
|
|
14
|
+
|
|
15
|
+
## Patterns
|
|
16
|
+
|
|
17
|
+
### 1. Check-then-act (TOCTOU)
|
|
18
|
+
- **Signature**: `if !exists(x) { create(x) }`, `if cache.get(k) is None: cache[k] = ...`, `os.path.exists` then `open`, `SELECT ... then INSERT`.
|
|
19
|
+
- **Why hard to find**: looks like ordinary control flow; only one window of vulnerability between two innocuous calls.
|
|
20
|
+
- **Where to look**: file create helpers, "ensure_" / "get_or_create" / "find_or_init", auth/session bootstrap, idempotent endpoints, DB upserts written as two statements.
|
|
21
|
+
- **Example**:
|
|
22
|
+
```python
|
|
23
|
+
if user_id not in sessions: # check
|
|
24
|
+
sessions[user_id] = Session() # act — two threads can both pass the check
|
|
25
|
+
```
|
|
26
|
+
- **Fix direction**: collapse to one atomic op (`setdefault`, `LoadOrStore`, `putIfAbsent`, DB unique constraint + `ON CONFLICT`, `O_CREAT|O_EXCL`).
|
|
27
|
+
- **Language variants**: Go → `sync.Map.LoadOrStore`; Java → `ConcurrentHashMap.computeIfAbsent`; Rust → `entry().or_insert_with`; Node → `fs.writeFile(... 'wx')`; SQL → unique index + upsert.
|
|
28
|
+
|
|
29
|
+
### 2. Non-atomic read-modify-write on shared state
|
|
30
|
+
- **Signature**: `counter++`, `total += x`, `list.append` on a module-level list (post-GIL Python), `map[k] = map[k] + 1`.
|
|
31
|
+
- **Why hard to find**: source line looks atomic; bug surfaces only under contention or load tests.
|
|
32
|
+
- **Where to look**: metrics counters, in-memory tallies, free-list updates, retry counters, ID generators.
|
|
33
|
+
- **Example**:
|
|
34
|
+
```go
|
|
35
|
+
var hits int
|
|
36
|
+
go func() { hits++ }() // racy — read, increment, write are three ops
|
|
37
|
+
```
|
|
38
|
+
- **Fix direction**: use atomics (`atomic.AddInt64`, `AtomicInteger.incrementAndGet`, `fetch_add`), a lock, or message-pass through a channel/actor.
|
|
39
|
+
- **Language variants**: Python 3.13 free-threading (PEP 703) removes the GIL safety net — `+=`, `dict[k]=...`, and `list.append` are no longer guaranteed atomic; audit shared module state.
|
|
40
|
+
|
|
41
|
+
### 3. Double-checked locking / unsafe publication
|
|
42
|
+
- **Signature**: `if instance is None: with lock: if instance is None: instance = X()`, lazy singletons.
|
|
43
|
+
- **Why hard to find**: looks correct; partially-constructed object may be visible to a reader without happens-before.
|
|
44
|
+
- **Where to look**: singleton getters, lazy caches, JNI/CGo glue, ObjC `+sharedInstance` rolled by hand.
|
|
45
|
+
- **Example**:
|
|
46
|
+
```java
|
|
47
|
+
if (cfg == null) { synchronized(L) { if (cfg == null) cfg = new Cfg(); } } // cfg must be volatile
|
|
48
|
+
```
|
|
49
|
+
- **Fix direction**: declare the field `volatile` / `AtomicReference` (Java), use `OnceLock`/`OnceCell` (Rust), `sync.Once` (Go), `dispatch_once` (Swift/ObjC), module-import time init (Python).
|
|
50
|
+
|
|
51
|
+
### 4. Memory visibility / missing happens-before
|
|
52
|
+
- **Signature**: a writer thread sets a `done`/`ready` flag; reader spins on it without `volatile`, `atomic`, or a release/acquire pair.
|
|
53
|
+
- **Why hard to find**: works on x86 due to strong ordering; fails on ARM/Apple Silicon or under aggressive JIT/compiler reordering.
|
|
54
|
+
- **Where to look**: shutdown flags, "ready" signals, `while (!stopped)` loops, lock-free queues, custom spinlocks, `static mut` in Rust.
|
|
55
|
+
- **Example**:
|
|
56
|
+
```cpp
|
|
57
|
+
bool ready=false; int v=0;
|
|
58
|
+
// Thread A: v=42; ready=true;
|
|
59
|
+
// Thread B: while(!ready); use(v); // both must be std::atomic with acquire/release
|
|
60
|
+
```
|
|
61
|
+
- **Fix direction**: use language-blessed atomics with appropriate ordering (`SeqCst` default, `Acquire`/`Release` if you know the model); in Java make fields `volatile` or use `j.u.c.atomic`; in Go use `atomic.LoadX`/`StoreX` or a channel.
|
|
62
|
+
|
|
63
|
+
### 5. Lock ordering / cyclical deadlock
|
|
64
|
+
- **Signature**: function F locks A then B; function G locks B then A.
|
|
65
|
+
- **Why hard to find**: only deadlocks when both paths run concurrently; tests usually take one path at a time.
|
|
66
|
+
- **Where to look**: any module with ≥2 mutexes; account/transfer code, parent↔child object graphs, bidirectional relationships, "swap" or "merge" operations.
|
|
67
|
+
- **Example**:
|
|
68
|
+
```go
|
|
69
|
+
// transfer(a,b): a.Lock(); b.Lock(); transfer(b,a) inverts — deadlock
|
|
70
|
+
```
|
|
71
|
+
- **Fix direction**: impose a global lock order (sort by address/ID), use a single coarser lock, use `try_lock` with back-off, or restructure to lock-free messaging.
|
|
72
|
+
|
|
73
|
+
### 6. Lock held during callback / I/O / await
|
|
74
|
+
- **Signature**: `lock.acquire(); callback(); lock.release()`, `mutex.lock(); http.get(...)`, holding a Tokio `MutexGuard` across `.await`.
|
|
75
|
+
- **Why hard to find**: callback may reenter, block, or call back into the same module, producing deadlock or pathological latency.
|
|
76
|
+
- **Where to look**: event emitters, observer/listener fan-out, plugin systems, async functions that take a `std::sync::Mutex`, Swift `@MainActor` work that calls completion handlers.
|
|
77
|
+
- **Example**:
|
|
78
|
+
```rust
|
|
79
|
+
let g = m.lock().unwrap();
|
|
80
|
+
client.get(url).await; // holds lock across await — task starvation / deadlock
|
|
81
|
+
```
|
|
82
|
+
- **Fix direction**: copy needed data out, drop the guard before the slow op, or use `tokio::sync::Mutex` (still avoid long holds); never invoke unknown user code under a lock.
|
|
83
|
+
|
|
84
|
+
### 7. Iteration during concurrent mutation
|
|
85
|
+
- **Signature**: `for k,v in map.items():` while another thread writes; `range m` in Go without a lock; `for (Entry e : map.entrySet())` on a non-concurrent map.
|
|
86
|
+
- **Why hard to find**: throws `ConcurrentModificationException` only sometimes; in Go produces silent corruption or panic.
|
|
87
|
+
- **Where to look**: stats dumpers, snapshot endpoints, cache eviction sweeps, GC-like cleanup loops.
|
|
88
|
+
- **Fix direction**: copy under lock then iterate the copy, use `ConcurrentHashMap`/`sync.Map`, or use snapshot iterators (`CopyOnWriteArrayList`).
|
|
89
|
+
|
|
90
|
+
### 8. Wait/notify / condition-variable misuse
|
|
91
|
+
- **Signature**: `if cond: wait()` (should be `while`), `notify_one()` when multiple waiters need wake, missing predicate check after wake.
|
|
92
|
+
- **Why hard to find**: spurious wakeups are rare; missed signals only fire under specific interleavings.
|
|
93
|
+
- **Where to look**: hand-rolled bounded queues, producer/consumer code, `Condvar`, `Object.wait/notify`, `threading.Condition`, `pthread_cond_t`.
|
|
94
|
+
- **Fix direction**: always `while (!predicate) wait()`, prefer `notify_all` when in doubt, or replace with a channel / `BlockingQueue` / semaphore.
|
|
95
|
+
|
|
96
|
+
### 9. Atomicity violation across multiple atomic ops
|
|
97
|
+
- **Signature**: `if map.contains(k) { map.get(k) }`, `atomic.Load` followed by `atomic.Store`, `ConcurrentHashMap.get` then `put`.
|
|
98
|
+
- **Why hard to find**: each individual op is atomic, so static analysis and review give a false sense of safety.
|
|
99
|
+
- **Where to look**: "get or update" code, balance/inventory math on atomics, two-step state transitions.
|
|
100
|
+
- **Fix direction**: use a CAS loop (`compareAndSet`, `compare_exchange`), `compute`/`merge` style atomic combinators, or take a lock spanning the full sequence.
|
|
101
|
+
|
|
102
|
+
### 10. Cancellation / interruption leaving partial state
|
|
103
|
+
- **Signature**: `try: work() except CancelledError: ...` without rollback; `ctx.Done()` mid-write; Go `context` cancel during multi-step DB write; Swift `Task.cancel`.
|
|
104
|
+
- **Why hard to find**: only fires when a timeout/cancel races with a critical section.
|
|
105
|
+
- **Where to look**: HTTP handlers with deadlines, batch jobs, transactional flows that mix DB + cache + external API, `defer`/`finally` blocks that themselves do I/O.
|
|
106
|
+
- **Fix direction**: make critical sections atomic (single tx), guard cleanup with `shield`/`uninterruptible`, persist intent before doing work so cancel-recovery is well-defined.
|
|
107
|
+
|
|
108
|
+
### 11. Channel deadlock / goroutine leak via blocked send/receive
|
|
109
|
+
- **Signature**: send to unbuffered channel with no receiver; receive from a channel that is never closed; `select` whose only ready case is `<-ctx.Done()` that never fires.
|
|
110
|
+
- **Why hard to find**: goroutines leak silently; only visible via pprof, RSS growth, or `runtime.NumGoroutine`.
|
|
111
|
+
- **Where to look**: fan-out workers without `WaitGroup`, request-scoped goroutines without `context`, "fire and forget" launches, Rust `tokio::spawn` whose `JoinHandle` is dropped.
|
|
112
|
+
- **Fix direction**: always pair sends with bounded receivers, close channels from the sender side, propagate `context.Context`, and use `errgroup` / `JoinSet` for structured concurrency.
|
|
113
|
+
|
|
114
|
+
### 12. `select` with `default` busy-loop / starvation
|
|
115
|
+
- **Signature**: Go `select { case x:=<-ch: ... ; default: }` inside a `for`; Rust `try_recv` polled in a tight loop; JS `while(!done)` with `setImmediate`.
|
|
116
|
+
- **Why hard to find**: code "works" but pins a CPU; starves siblings on the same runtime.
|
|
117
|
+
- **Where to look**: hand-rolled pollers, retry loops, scheduler shims, custom event loops.
|
|
118
|
+
- **Fix direction**: drop `default` and block on the channel, add a `time.After` / `tokio::time::sleep`, or restructure as event-driven.
|
|
119
|
+
|
|
120
|
+
### 13. Caching: stampede / thundering herd / double-fill
|
|
121
|
+
- **Signature**: many concurrent misses for the same key each trigger a backend fetch; `if cache.miss: load_from_db()` with no in-flight coalescing.
|
|
122
|
+
- **Why hard to find**: behaves fine under low load; collapses the backing store under traffic spikes or cold start.
|
|
123
|
+
- **Where to look**: memoization decorators, HTTP middleware caches, CDN-fronted endpoints, session/JWT lookups.
|
|
124
|
+
- **Fix direction**: single-flight / request coalescing (`singleflight.Group`, `asyncio` future cache, Caffeine `AsyncLoadingCache`), jittered TTLs, lock-on-key, probabilistic early expiration.
|
|
125
|
+
|
|
126
|
+
### 14. Send/Sync violations and !Send across await (Rust)
|
|
127
|
+
- **Signature**: `Rc<...>` shared across threads; `RefCell` behind `Arc`; `MutexGuard`, `*mut T`, or non-`Send` future held across `.await` on a multi-threaded runtime.
|
|
128
|
+
- **Why hard to find**: compiler usually catches `!Send`, but trait-object boundaries (`Box<dyn Future>`), conditional compilation, and `unsafe impl Send` can hide it.
|
|
129
|
+
- **Where to look**: `unsafe impl Send for ...`, `tokio::spawn` of large async blocks, FFI handles, `Pin<Box<dyn Future>>` returned from traits, async-trait macros.
|
|
130
|
+
- **Fix direction**: replace `Rc/RefCell` with `Arc/Mutex` (or `tokio::sync::Mutex` if held across await), restrict to `LocalSet` / single-thread runtime, narrow guard scope, audit every `unsafe impl Send`.
|
|
131
|
+
|
|
132
|
+
### 15. Reentrancy and re-entering the same lock
|
|
133
|
+
- **Signature**: a method takes a lock then calls a virtual/overridden method that takes the same lock; recursive locking on non-reentrant mutex (`std::sync::Mutex`, `pthread_mutex_t` default).
|
|
134
|
+
- **Why hard to find**: works if the second call ends up on a different instance; deadlocks only on certain object graphs.
|
|
135
|
+
- **Where to look**: framework templates, ORM lifecycle hooks, ObjC KVO observers, signals/slots, `Drop` impls that lock.
|
|
136
|
+
- **Fix direction**: split critical sections so callbacks run outside; use a re-entrant mutex only as a stopgap; restructure ownership.
|
|
137
|
+
|
|
138
|
+
### 16. Background task lifetime escapes parent scope
|
|
139
|
+
- **Signature**: spawning a task that captures `self` / request-scoped state then outlives it; `setTimeout(fn, 0)` in a handler that finishes; `tokio::spawn` not joined.
|
|
140
|
+
- **Why hard to find**: works in tests because the process stays alive; production crashes are use-after-free or stale-data writes.
|
|
141
|
+
- **Where to look**: request handlers, React `useEffect` async work, Swift `Task { ... }` not stored, fire-and-forget logging/metrics flushes.
|
|
142
|
+
- **Fix direction**: structured concurrency (`errgroup`, `TaskGroup`, `JoinSet`, `AbortController`), explicit join/cancel on parent exit, tie task lifetime to a `context.Context`.
|
|
143
|
+
|
|
144
|
+
### 17. Lost update on optimistic concurrency without version check
|
|
145
|
+
- **Signature**: `row = SELECT ...; row.x += 1; UPDATE SET x = row.x WHERE id = ?` with no `version`/`updated_at` predicate.
|
|
146
|
+
- **Why hard to find**: classic last-writer-wins; only manifests as silent data drift.
|
|
147
|
+
- **Where to look**: counters, inventory, balances, "edit settings" endpoints, ETL merge jobs.
|
|
148
|
+
- **Fix direction**: add a `version` column + `WHERE version = ?` and retry on 0 rows; or `UPDATE ... SET x = x + 1`; or `SELECT ... FOR UPDATE`.
|