crewforth 3.3.0 → 3.5.0-rc.1
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/CHANGELOG.md +271 -0
- package/README.md +7 -2
- package/VERSION +1 -1
- package/adopt.sh +40 -5
- package/bin/cli.js +193 -2
- package/kit/AGENT_TEMPLATE.md +15 -0
- package/kit/CLAUDE.md +49 -17
- package/kit/agents/crew-backend-expert.md +4 -3
- package/kit/agents/crew-commit-agent.md +2 -0
- package/kit/agents/crew-database-expert.md +2 -1
- package/kit/agents/crew-devops-expert.md +1 -0
- package/kit/agents/crew-docs-agent.md +3 -1
- package/kit/agents/crew-frontend-expert.md +2 -0
- package/kit/agents/crew-performance-expert.md +7 -2
- package/kit/agents/crew-planner.md +15 -3
- package/kit/agents/crew-privacy-agent.md +2 -0
- package/kit/agents/crew-review-agent.md +8 -1
- package/kit/agents/crew-security-expert.md +4 -3
- package/kit/agents/crew-test-expert.md +6 -2
- package/kit/eval/doctor.sh +201 -1
- package/kit/eval/gate-report.sh +28 -0
- package/kit/eval/golden-holdout.txt +2 -0
- package/kit/eval/golden-routing.txt +17 -0
- package/kit/eval/lib/crew-map/contracts.mjs +77 -0
- package/kit/eval/lib/crew-map/crew-map.sh +2 -1
- package/kit/eval/lib/crew-map/detect.mjs +2 -1
- package/kit/eval/lib/crew-map/narrative.mjs +14 -3
- package/kit/eval/lib/crew-map/project.mjs +45 -6
- package/kit/eval/lib/crew-map/scan.mjs +153 -14
- package/kit/eval/lib/findings/findings.schema.json +11 -1
- package/kit/eval/lib/findings/findings.sh +24 -1
- package/kit/eval/lib/hook-inject.sh +159 -0
- package/kit/eval/lib/lockfiles.sh +71 -0
- package/kit/eval/lib/payload-check.sh +29 -0
- package/kit/eval/lib/preview.sh +3 -3
- package/kit/eval/lib/review-bench/bench.mjs +14 -2
- package/kit/eval/lib/review-prepass/prepass.mjs +32 -3
- package/kit/eval/lib/settings-json.awk +18 -0
- package/kit/eval/lib/untrusted-text.awk +38 -0
- package/kit/eval/scan-skill.sh +67 -20
- package/kit/eval/smoke-test.sh +1792 -52
- package/kit/eval/utilization.sh +77 -3
- package/kit/hooks/agent-outcome.sh +9 -1
- package/kit/hooks/board-sync.sh +49 -3
- package/kit/hooks/board.sh +8 -8
- package/kit/hooks/branch-fresh.sh +101 -0
- package/kit/hooks/floor-blocklist.txt +19 -1
- package/kit/hooks/guard-bash.sh +92 -8
- package/kit/hooks/guard-question.sh +2 -1
- package/kit/hooks/loop-guard.sh +445 -0
- package/kit/hooks/map-stale.sh +437 -0
- package/kit/hooks/pre-commit +48 -7
- package/kit/hooks/precompact-snapshot.sh +102 -6
- package/kit/hooks/prompt-approval.sh +45 -9
- package/kit/hooks/route-hint.sh +36 -7
- package/kit/hooks/secret-blocklist.txt +55 -1
- package/kit/hooks/session-guard.sh +85 -28
- package/kit/hooks/session-rehydrate.sh +34 -11
- package/kit/hooks/session-stats.sh +169 -8
- package/kit/hooks/session-update-check.sh +5 -1
- package/kit/hooks/skill-trust.sh +23 -14
- package/kit/hooks/turn-check.sh +190 -0
- package/kit/hooks/worktree-provision.sh +91 -0
- package/kit/owned-blobs.tsv +4 -2
- package/kit/settings.json +86 -0
- package/kit/skills/a11y/SKILL.md +50 -3
- package/kit/skills/a11y/references/recipes.md +41 -0
- package/kit/skills/adr/SKILL.md +14 -2
- package/kit/skills/api-design/SKILL.md +6 -1
- package/kit/skills/api-design/references/idempotency.md +39 -0
- package/kit/skills/brainstorm/SKILL.md +4 -2
- package/kit/skills/brainstorm/references/asking.md +47 -0
- package/kit/skills/ci-pipeline/SKILL.md +11 -1
- package/kit/skills/ci-pipeline/references/agent-job.md +33 -0
- package/kit/skills/commit-message/SKILL.md +9 -3
- package/kit/skills/confidence-check/SKILL.md +9 -7
- package/kit/skills/confidence-check/references/source-check.md +37 -0
- package/kit/skills/crew-approve/SKILL.md +3 -0
- package/kit/skills/crew-audit/SKILL.md +63 -0
- package/kit/skills/crew-board/SKILL.md +2 -2
- package/kit/skills/crew-code-review/SKILL.md +27 -4
- package/kit/skills/crew-code-review/references/panel-mode.md +9 -9
- package/kit/skills/crew-code-review/references/simplify.md +28 -0
- package/kit/skills/crew-doctor/SKILL.md +3 -2
- package/kit/skills/crew-gates/SKILL.md +3 -0
- package/kit/skills/crew-gates/references/enforcers.md +30 -0
- package/kit/skills/crew-interview/SKILL.md +42 -0
- package/kit/skills/crew-map/SKILL.md +26 -6
- package/kit/skills/crew-plan/SKILL.md +9 -2
- package/kit/skills/crew-pr/SKILL.md +57 -0
- package/kit/skills/crew-pr/references/checks.md +35 -0
- package/kit/skills/crew-pr/references/threads.md +23 -0
- package/kit/skills/crew-review/SKILL.md +34 -1
- package/kit/skills/crew-ship/SKILL.md +5 -2
- package/kit/skills/crew-skill/SKILL.md +26 -1
- package/kit/skills/crew-update/SKILL.md +4 -1
- package/kit/skills/db-migration/SKILL.md +15 -0
- package/kit/skills/db-migration/references/expand-contract.md +52 -0
- package/kit/skills/dependency-audit/SKILL.md +34 -1
- package/kit/skills/dependency-audit/scripts/record.sh +37 -0
- package/kit/skills/dependency-upgrade/SKILL.md +29 -1
- package/kit/skills/deploy/SKILL.md +18 -0
- package/kit/skills/deploy/references/topology-a.md +7 -0
- package/kit/skills/deploy/references/topology-b.md +2 -2
- package/kit/skills/docs-writer/SKILL.md +37 -2
- package/kit/skills/eval-grader/SKILL.md +9 -0
- package/kit/skills/eval-grader/references/method.md +26 -0
- package/kit/skills/frontend/SKILL.md +5 -2
- package/kit/skills/frontend/references/from-design.md +29 -0
- package/kit/skills/frontend/references/page-metadata.md +17 -0
- package/kit/skills/frontend/references/verify-contract.md +18 -1
- package/kit/skills/frontend/references/worst-data.md +35 -0
- package/kit/skills/frontend-design/SKILL.md +20 -4
- package/kit/skills/frontend-design/references/design-review.md +21 -0
- package/kit/skills/frontend-design/references/detection.md +44 -0
- package/kit/skills/frontend-design/references/motion.md +54 -0
- package/kit/skills/frontend-design/references/ui-copy.md +35 -0
- package/kit/skills/frontend-rn-expo/SKILL.md +3 -0
- package/kit/skills/handoff/SKILL.md +28 -3
- package/kit/skills/handoff/references/cold-reader.md +13 -6
- package/kit/skills/i18n-integrity/SKILL.md +3 -0
- package/kit/skills/iterate/SKILL.md +7 -2
- package/kit/skills/mcp-builder/SKILL.md +21 -4
- package/kit/skills/mcp-builder/references/building.md +74 -13
- package/kit/skills/mcp-builder/references/cli-for-agents.md +26 -0
- package/kit/skills/observability/SKILL.md +13 -1
- package/kit/skills/performance/SKILL.md +25 -1
- package/kit/skills/performance/references/bottlenecks.md +11 -0
- package/kit/skills/performance/references/web-vitals.md +31 -0
- package/kit/skills/red-team/SKILL.md +25 -3
- package/kit/skills/reflect/SKILL.md +7 -2
- package/kit/skills/release/SKILL.md +13 -0
- package/kit/skills/security-scan/SKILL.md +6 -2
- package/kit/skills/security-scan/references/ai-agents.md +16 -0
- package/kit/skills/security-scan/references/fronts.md +59 -2
- package/kit/skills/security-scan/references/reporting.md +13 -0
- package/kit/skills/security-scan/references/verify.md +9 -0
- package/kit/skills/sonarqube-check/SKILL.md +32 -2
- package/kit/skills/sonarqube-check/references/ratchet.md +32 -0
- package/kit/skills/spec-planning/SKILL.md +25 -8
- package/kit/skills/systematic-debugging/SKILL.md +15 -7
- package/kit/skills/systematic-debugging/references/techniques.md +5 -0
- package/kit/skills/teamboard/SKILL.md +3 -3
- package/kit/skills/testing/SKILL.md +18 -3
- package/kit/skills/testing/references/browser-headless.md +21 -0
- package/kit/skills/token-budget/SKILL.md +26 -5
- package/kit/skills/worktree/SKILL.md +13 -5
- package/kit/studio/server/lib/session.js +23 -2
- package/kit/studio/server/lib/worktree-changes.js +64 -0
- package/kit/studio/web/convo.js +14 -0
- package/kit/studio/web/map-plan.js +54 -8
- package/kit/studio/web/map.js +15 -3
- package/package.json +5 -4
- package/start.sh +11 -2
|
@@ -15,10 +15,18 @@
|
|
|
15
15
|
**Finding format** (ranked by severity):
|
|
16
16
|
```
|
|
17
17
|
[CRITICAL] SQL Injection — src/api/users.ts:42
|
|
18
|
+
CWE : CWE-89
|
|
18
19
|
Vuln : Input concatenated directly into the SQL query
|
|
19
20
|
Impact: DB can be read / modified / deleted
|
|
20
21
|
Fix : Use a parameterized query
|
|
22
|
+
Depth : Least-privilege DB user for this service (limits the damage if another path is missed)
|
|
23
|
+
Detect: Alert on SQL syntax errors from this endpoint
|
|
24
|
+
Retest: Send `' OR 1=1--` as the id; expect 400 and no rows
|
|
21
25
|
```
|
|
26
|
+
`CWE` names the weakness class, so findings can be counted and compared across reports. `Fix` closes the hole;
|
|
27
|
+
`Depth` (defense in depth) and `Detect` (how an attempt would be seen) are optional and never replace it.
|
|
28
|
+
`Retest` is the one check that proves the fix, written so someone else can run it. Several findings of the same
|
|
29
|
+
class (ten unescaped outputs) may share one entry with a table of locations instead of ten copies.
|
|
22
30
|
|
|
23
31
|
**Summary line:**
|
|
24
32
|
```
|
|
@@ -66,6 +74,11 @@ Read it before planning, and let it shape priority — never coverage.
|
|
|
66
74
|
- A previous `partial` or `unknown` row is never inherited as `complete`, and a previous narrow or quick pass is
|
|
67
75
|
never read as "the rest was fine". Say in this report which rows came from the earlier one and why they still hold.
|
|
68
76
|
|
|
77
|
+
When this pass closes out earlier findings, add a retest table, one row per finding, each with its `Retest`
|
|
78
|
+
result: `fixed-verified` (the retest ran and failed to reproduce) · `partial` (one path closed, another open) ·
|
|
79
|
+
`not-fixed` · `accepted-risk` (the user's recorded decision, with where it is recorded). A fix nobody retested
|
|
80
|
+
is `not-fixed` until it is.
|
|
81
|
+
|
|
69
82
|
## Fix presentation
|
|
70
83
|
```
|
|
71
84
|
How shall we proceed?
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Verify before you report — the adversarial pass
|
|
2
2
|
|
|
3
|
+
## Contents
|
|
4
|
+
- The verifier stance
|
|
5
|
+
- The 4-step verify procedure (each step kills a specific false-positive class)
|
|
6
|
+
- Verdict — three outcomes, never a forced binary
|
|
7
|
+
- Exclusion rules — FALSE_POSITIVE even if technically accurate
|
|
8
|
+
- Structured verdict block (one per finding, parseable)
|
|
9
|
+
- Tally
|
|
10
|
+
- Severity is a SEPARATE judgment from verification
|
|
11
|
+
|
|
3
12
|
Discovery and verification want opposite things. **Discovery is recall-biased** — when unsure whether something is
|
|
4
13
|
real, surface it (a low-confidence finding beats a missed bug). **Verification is precision-biased** — a separate,
|
|
5
14
|
adversarial pass that tries to *disprove* each finding. Keeping them separate is the single biggest lever on false
|
|
@@ -44,7 +44,22 @@ Scan with whatever runner the project's stack uses — the scanner is the same p
|
|
|
44
44
|
| Any stack (generic) | `sonar-scanner -Dsonar.projectKey=app -Dsonar.sources=. -Dsonar.host.url=http://localhost:9000 -Dsonar.token=$T` |
|
|
45
45
|
| Maven / Gradle | `mvn sonar:sonar -Dsonar.host.url=… -Dsonar.token=$T` · `gradle sonarqube …` |
|
|
46
46
|
| .NET | `dotnet sonarscanner begin /k:app /d:sonar.host.url=… /d:sonar.token=$T` → `dotnet build` → `dotnet sonarscanner end /d:sonar.token=$T` |
|
|
47
|
-
| JS/TS, Python, Go, PHP, Ruby | the generic `sonar-scanner` above
|
|
47
|
+
| JS/TS, Python, Go, PHP, Ruby | the generic `sonar-scanner` above, with the coverage report below |
|
|
48
|
+
|
|
49
|
+
**Coverage first: SonarQube runs no tests, it reads a report.** Produce one before the scan and pass its path, or
|
|
50
|
+
the analysis counts every line uncovered and prints 0.0%:
|
|
51
|
+
|
|
52
|
+
| Stack | Produce | Pass to the scanner |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Python | `pytest --cov=<package> --cov-report=xml` (needs `pytest-cov`) | `-Dsonar.python.coverage.reportPaths=coverage.xml` |
|
|
55
|
+
| JS/TS | `vitest run --coverage --coverage.reporter=lcov` · `jest --coverage` | `-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info` |
|
|
56
|
+
| .NET | coverlet, OpenCover format | `/d:sonar.cs.opencover.reportsPaths=…` (*reports*, with an s) |
|
|
57
|
+
| Java | JaCoCo XML report | `-Dsonar.coverage.jacoco.xmlReportPaths=…` |
|
|
58
|
+
| Go | `go test -coverprofile=coverage.out ./...` | `-Dsonar.go.coverage.reportPaths=coverage.out` |
|
|
59
|
+
|
|
60
|
+
**No report, no coverage figure.** When none was produced (or `lines_to_cover` equals `uncovered_lines`), write
|
|
61
|
+
*"coverage: not measured — no report was passed to the analysis"*, never "0%": a 0 there says the tests were not
|
|
62
|
+
read, not that there are none. Installing the coverage plugin is a proposal like any other tool.
|
|
48
63
|
|
|
49
64
|
**c) No server is possible at all** (no Docker, no Java, no permission to run one) → run the language's Sonar-rule
|
|
50
65
|
engine locally as a pre-check (**`references/local-engines.md`** — which engine per language, and how close it is
|
|
@@ -52,7 +67,8 @@ to Sonar's own rules) and **say what that is worth**, in these words rather than
|
|
|
52
67
|
rating: *"the local rule engine reports N issues, none open; the SonarQube rating and the gate were not produced —
|
|
53
68
|
no analysis ran."* Then list what stays unverified: taint/injection, hotspot review state, coverage, duplication,
|
|
54
69
|
files outside the compiled language, and the project's own quality profile. Offer branch (b) — a local server, with
|
|
55
|
-
or without Docker — as the way to close it.
|
|
70
|
+
or without Docker — as the way to close it. Until one runs, a committed baseline keeps the numbers the project can
|
|
71
|
+
measure from getting worse: **`references/ratchet.md`**.
|
|
56
72
|
|
|
57
73
|
**Never invent the verdict.** A rating exists only where an analysis produced one; without it the honest output is
|
|
58
74
|
"unverified", and that is the sentence that saves the user from promising a clean report they cannot back.
|
|
@@ -71,6 +87,16 @@ Also read the gate's definition (`/api/qualitygates/show`): most gates judge **n
|
|
|
71
87
|
Chasing overall counts while the gate measures new code burns days and still fails; the reverse leaves real debt.
|
|
72
88
|
Know which one applies before planning the work.
|
|
73
89
|
|
|
90
|
+
**An empty new-code period makes "Passed" mean nothing.** On a first analysis, or under `PREVIOUS_VERSION` while
|
|
91
|
+
the version has not changed, `project_status` returns `"period": null` and every `new_*` condition comes back
|
|
92
|
+
`actualValue: null`, `status: OK` — the gate passes because there is no new code to judge, while the project may
|
|
93
|
+
carry an E rating. When that is what you read:
|
|
94
|
+
- report the **overall** ratings and counts (the `measures` call above) as the verdict, and say the gate had no new
|
|
95
|
+
code to evaluate — never "the gate passed";
|
|
96
|
+
- propose a baseline so the next analysis has new code to judge: `curl -sfu "$T:" -X POST
|
|
97
|
+
"$S/api/new_code_periods/set?project=$K&type=NUMBER_OF_DAYS&value=30"` (or `REFERENCE_BRANCH` with the main branch).
|
|
98
|
+
It changes the project's settings, so it is the user's yes, like any other change.
|
|
99
|
+
|
|
74
100
|
## Step 3 — fix by rule, never by impression
|
|
75
101
|
|
|
76
102
|
Group findings by `rule` (`csharpsquid:S1481`, `typescript:S3776`, `python:S5852`, …) and take one rule at a time,
|
|
@@ -105,4 +131,8 @@ is not proof of injection safety: cover that with the **`security-scan`** and **
|
|
|
105
131
|
- Hotspots reviewed in the UI, not merely edited around.
|
|
106
132
|
- A re-scan ran after the fixes and the before → after diff by rule id is reported.
|
|
107
133
|
- Injection risk stated separately (taint analysis is not in Community Build).
|
|
134
|
+
- Coverage is a measured figure from a report passed to the analysis, or written "not measured" — never a bare 0%.
|
|
135
|
+
- A gate read with `"period": null` is reported by its overall ratings, not as "passed".
|
|
136
|
+
- The project's `## Stack` → `Quality analysis:` line says which path ran (`sonarqube-local`, `sonarqube-server`,
|
|
137
|
+
or `declined`); an empty one is asked once and written there.
|
|
108
138
|
- Anything not done is said plainly — never implied clean.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# A ratchet when no analysis can run
|
|
2
|
+
|
|
3
|
+
Loaded on demand from SKILL.md branch (c). It does not replace an analysis and never produces a rating: it keeps
|
|
4
|
+
the numbers the project CAN measure from getting worse while no server is available.
|
|
5
|
+
|
|
6
|
+
## The rule
|
|
7
|
+
Record today's value of each number the project measures without a server, commit it, and fail only when a change
|
|
8
|
+
makes it worse by more than its tolerance. Improving it moves the baseline; worsening it needs a decision.
|
|
9
|
+
|
|
10
|
+
| Number | Where it comes from | Usual tolerance |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| line coverage | the test runner's report (lcov, cobertura, coverage.xml) | 0.5 points |
|
|
13
|
+
| lint findings | the local rule engine (`references/local-engines.md`), by rule id | 0 new |
|
|
14
|
+
| bundle or binary size | the build output | 2 % |
|
|
15
|
+
| build warnings | the compiler | 0 new |
|
|
16
|
+
|
|
17
|
+
## The baseline file
|
|
18
|
+
One committed file (for example `.quality-baseline.json`) holds, per number: the value, the command that measured it,
|
|
19
|
+
and the commit it was measured at. A number nobody can re-measure with the command written there is not a baseline.
|
|
20
|
+
|
|
21
|
+
## Exceptions
|
|
22
|
+
A regression the team accepts is written into the same file with an owner and an expiry date, and the reason. An
|
|
23
|
+
exception past its date fails like the regression it covered. There is no exception without an owner.
|
|
24
|
+
|
|
25
|
+
## What it is worth, said out loud
|
|
26
|
+
"Coverage 81.2 % against a baseline of 81.0 %, 0 new lint findings: no regression. No SonarQube analysis ran; the
|
|
27
|
+
gate and the ratings were not produced." A ratchet that passes is not a clean analysis, and the report says so.
|
|
28
|
+
|
|
29
|
+
## At least one check from outside
|
|
30
|
+
A ratchet only compares the project with itself. At least one check that does not come from the project's own
|
|
31
|
+
tooling still runs: the CI's own build, a dependency audit, or the local rule engine, so a number that drifted in
|
|
32
|
+
the baseline file is not the only witness.
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-planning
|
|
3
3
|
description: |
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Use when the scope is unclear or the work spans more than one change.
|
|
4
|
+
Use when work needs a plan before code: the user asks how to split it into tasks or what the acceptance
|
|
5
|
+
criteria are, or the scope spans more than one change.
|
|
7
6
|
---
|
|
8
7
|
|
|
9
8
|
# Spec-First Planning
|
|
@@ -18,25 +17,43 @@ Before writing code: what will be done, how it counts as "done", and in what ord
|
|
|
18
17
|
2. **Split into vertical slices:** the smallest end-to-end working pieces (not horizontal layers). Each slice delivers value on its own.
|
|
19
18
|
3. **A contract for every task:** input · output · **measurable acceptance criterion** (testable) · estimated risk.
|
|
20
19
|
4. **Dependency graph:** which task waits on what; no cycles. **Bring the riskiest/most-unknown to the front** (fail-fast).
|
|
21
|
-
5. **Uncertainties:** assumption list + open questions; do not fill ambiguous spots with a guess, ask with explicit options
|
|
20
|
+
5. **Uncertainties:** assumption list + open questions; do not fill ambiguous spots with a guess, ask with explicit options
|
|
21
|
+
(how to ask, and what an unanswered question becomes: `brainstorm/references/asking.md`).
|
|
22
22
|
|
|
23
23
|
## Output (docs/PLAN.md)
|
|
24
|
+
**Before writing:** a `docs/PLAN.md` that still has unchecked tasks for other work is not overwritten. Stop and ask
|
|
25
|
+
whether to replace it, add this plan to it, or write this one beside it.
|
|
24
26
|
```
|
|
25
27
|
# <Feature> — Plan
|
|
28
|
+
commit: <HEAD when the plan was written>
|
|
26
29
|
## Acceptance criteria
|
|
27
|
-
- [ ] AC-1: <measurable outcome>
|
|
28
|
-
- [ ] AC-2: <measurable outcome>
|
|
30
|
+
- [ ] AC-1: <measurable outcome> — proven by: <the one test level: unit · integration · e2e · a named manual check>
|
|
31
|
+
- [ ] AC-2: <measurable outcome> — proven by: <…>
|
|
32
|
+
## Constraints
|
|
33
|
+
- <a value, limit or name copied word for word from the request or spec>
|
|
29
34
|
## Tasks (order)
|
|
30
|
-
1. <task> — satisfies: AC-1 — dependency: <none/#n> — risk: <low/medium/high>
|
|
35
|
+
1. <task> — satisfies: AC-1 — size: <S/M/L> — dependency: <none/#n> — risk: <low/medium/high>
|
|
36
|
+
Interfaces: consumes <name: type> · produces <name: type> (only when a later task uses its output)
|
|
37
|
+
Current code: <a few lines, verbatim, at the place it changes> (optional)
|
|
38
|
+
2. …
|
|
39
|
+
— checkpoint: build and tests green, the criteria so far re-read — (after every 2-3 tasks)
|
|
40
|
+
## Not covered by tests
|
|
41
|
+
- <an input or case no test reaches> → task <n> (five at most)
|
|
31
42
|
## Assumptions / Open questions
|
|
32
43
|
- ...
|
|
33
44
|
```
|
|
45
|
+
- **No timeline and no estimate in time.** A size (S/M/L) says how much, never when.
|
|
46
|
+
- **No code beyond signatures.** A plan names functions and types; the bodies are the tasks' work. The only code a
|
|
47
|
+
plan quotes is the current code a task changes, word for word.
|
|
48
|
+
- **Drift stops the work.** When a task's `Current code` no longer matches the file, or its files changed after the
|
|
49
|
+
plan's `commit:`, the task stops and reports what moved. It does not improvise a new version of the plan.
|
|
34
50
|
|
|
35
51
|
**Every criterion carries a stable id, and every task names the ids it satisfies.** The id is what makes the plan
|
|
36
52
|
checkable later: "AC-2 is only partly built" can be said, argued about and tracked; "the second checkbox" cannot,
|
|
37
53
|
and renumbering a list silently re-points every reference to it. Never reuse or renumber an id — a dropped
|
|
38
54
|
criterion keeps its id with a strike-through and a one-line reason. A criterion no task satisfies, and a task that
|
|
39
|
-
satisfies no criterion, are both findings before a line of code is written
|
|
55
|
+
satisfies no criterion, are both findings before a line of code is written: the task is cut, or raised as an open
|
|
56
|
+
question.
|
|
40
57
|
|
|
41
58
|
## Mark what you do not know — do not fill it in
|
|
42
59
|
Where a requirement admits more than one reading, write the marker **`[NEEDS CLARIFICATION: <the question>]`** at
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: systematic-debugging
|
|
3
3
|
description: |
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Use when a bug's cause is unknown: something is broken, crashes, or fails only sometimes, or a few fixes
|
|
5
|
+
have already been tried. Use before changing code to make a failure go away.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Systematic Debugging
|
|
@@ -19,13 +19,20 @@ This skill is the discipline that turns "try things until it works" into "unders
|
|
|
19
19
|
> project's review/test gates. Don't disable a test or a gate to make a symptom pass (that is masking, not fixing).
|
|
20
20
|
|
|
21
21
|
## The loop (do them in order — skipping a step is why bugs come back)
|
|
22
|
+
0. **Settle intent** — is the expected behaviour written down? Check the ADRs, the README and the tests: *documented*
|
|
23
|
+
(it is a bug, go on), *contradicted* (it was never meant to work this way: a feature request, back to
|
|
24
|
+
**crew-planner**), or *silent* (ask the user with AskUserQuestion before changing anything).
|
|
22
25
|
1. **Reproduce** — a reliable, minimal repro. Can't reproduce → that IS the first problem; see `references/techniques.md` (intermittent/heisenbug).
|
|
23
26
|
2. **Isolate** — shrink the surface until the failure is in the smallest possible slice (bisect commits, halve the input, disable half the system).
|
|
24
|
-
3. **Hypothesize** — state ONE falsifiable cause: "X fails because Y." Write it down. A vague hunch is not a hypothesis.
|
|
27
|
+
3. **Hypothesize** — state ONE falsifiable cause: "X fails because Y." Write it down. A vague hunch is not a hypothesis. (`hooks/loop-guard.sh` points back here when a turn repeats one failure.)
|
|
25
28
|
4. **Test the hypothesis** — the cheapest observation that would *disprove* it (a log, a breakpoint, a probe). If it survives, you have the cause; if not, back to 3.
|
|
26
29
|
5. **Confirm the root cause** — you can explain the full chain symptom←…←cause, and you can turn the bug on and off by touching the cause.
|
|
27
|
-
6. **Fix** — the smallest change at the cause (not the symptom). Consider what else shares that cause
|
|
28
|
-
|
|
30
|
+
6. **Fix** — the smallest change at the cause (not the symptom). Consider what else shares that cause: grep every
|
|
31
|
+
caller of the function you fix, and fix it once in the shared code rather than at each caller.
|
|
32
|
+
7. **Verify** — the original repro now passes, a regression test locks it, and nothing nearby broke. The regression
|
|
33
|
+
test is shown **failing with the fix reverted and passing with it**: one that is green on both sides proves
|
|
34
|
+
nothing. Reproduce through the entry point the bug came in by (the CLI, the handler, the parser's real output),
|
|
35
|
+
not by handing a function a value its callers never produce.
|
|
29
36
|
|
|
30
37
|
## Checklist
|
|
31
38
|
- [ ] Reliable repro captured (exact steps/input/env)
|
|
@@ -33,7 +40,7 @@ This skill is the discipline that turns "try things until it works" into "unders
|
|
|
33
40
|
- [ ] One falsifiable hypothesis written down
|
|
34
41
|
- [ ] Hypothesis tested by observation (not by applying a fix and seeing)
|
|
35
42
|
- [ ] Root cause confirmed (can toggle the bug via the cause; full chain explained)
|
|
36
|
-
- [ ] Fix at the cause + regression test added
|
|
43
|
+
- [ ] Fix at the cause + regression test added, seen red without the fix and green with it
|
|
37
44
|
- [ ] Original repro passes; no new failures
|
|
38
45
|
|
|
39
46
|
## Anti-patterns (each is guess-driven patching wearing a disguise)
|
|
@@ -53,5 +60,6 @@ resource), the hypothesis log, and when to stop and ask for a second pair of eye
|
|
|
53
60
|
1. **No fix without a confirmed root cause** — the one rule; everything else serves it.
|
|
54
61
|
2. **One change at a time** — isolate cause and effect; never shotgun.
|
|
55
62
|
3. **Fix the cause, not the symptom** — a guard that hides the failure is not a fix.
|
|
56
|
-
4. **Lock it with a regression test** — the repro becomes a test so it can't silently return
|
|
63
|
+
4. **Lock it with a regression test** — the repro becomes a test so it can't silently return, and it was seen failing
|
|
64
|
+
with the fix reverted.
|
|
57
65
|
5. **Never mask to go green** — don't disable a test/gate/assertion to make the symptom pass.
|
|
@@ -15,6 +15,7 @@ Loaded on demand — the SKILL.md loop is the method; this is the toolbox for a
|
|
|
15
15
|
- **Debugger/breakpoint** — best for a reliable, local repro where you want to inspect live state and step. Conditional breakpoints (`x == badValue`) skip the noise.
|
|
16
16
|
- **Assertions/invariants** — add a check that *should* always hold; the point it fires is upstream of the symptom.
|
|
17
17
|
- **Bisect on values** — binary-search *when* a value goes wrong: log at start/middle/end of the pipeline, then narrow.
|
|
18
|
+
- **Boundary logging** — when a value crosses several layers (config → loader → service → handler), log what enters and what leaves each layer, run once, and read where the value first differs from what was sent. The layer where it changes owns the bug.
|
|
18
19
|
|
|
19
20
|
## Intermittent / heisenbugs (can't reliably reproduce)
|
|
20
21
|
|
|
@@ -41,4 +42,8 @@ Root cause: <the confirmed chain> Fix: <change at cause> Test: <regression tes
|
|
|
41
42
|
|
|
42
43
|
## When to stop and get a second pair of eyes
|
|
43
44
|
|
|
45
|
+
**Three fixes that each moved the failure somewhere else** is a different stop: the defect is in the design, not in
|
|
46
|
+
any one line. Stop patching and put the design question to the user with AskUserQuestion, with the log of the three
|
|
47
|
+
fixes and where each one moved the failure.
|
|
48
|
+
|
|
44
49
|
After ~3 disproved hypotheses with no new information, or when you've been staring at the same 20 lines — you likely hold a wrong assumption you can't see. Escalate: hand over the hypothesis log (not "it's broken") so the next person starts from what's *already ruled out*, not from zero. In Crewforth, that handover is the `handoff` skill.
|
|
@@ -4,7 +4,7 @@ description: |
|
|
|
4
4
|
Experimental: a shared team board. Claim a work item, hand it over, finish it; the claim is a git-ref
|
|
5
5
|
lock, so two people cannot take the same item.
|
|
6
6
|
metadata:
|
|
7
|
-
experimental: true
|
|
7
|
+
experimental: "true"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Team Board (shared claims + shared item memory)
|
|
@@ -66,8 +66,8 @@ applies unchanged: strip secrets, tokens, credentials and personal notes, leave
|
|
|
66
66
|
|
|
67
67
|
## Filling the board
|
|
68
68
|
Items come from either direction, and both end at `add`:
|
|
69
|
-
- **From a plan:** `crew-planner` + `spec-planning` produce `docs/PLAN.md` with measurable acceptance
|
|
70
|
-
a dependency order; each task becomes one `add <id> <title> <deps>`. Keep the plan's dependency edges — they
|
|
69
|
+
- **From a plan:** `crew-planner` + `spec-planning` produce the plan for `docs/PLAN.md` with measurable acceptance
|
|
70
|
+
criteria and a dependency order; each task becomes one `add <id> <title> <deps>`. Keep the plan's dependency edges — they
|
|
71
71
|
are what makes `claim` able to refuse blocked work.
|
|
72
72
|
- **By hand:** the user dictates the items.
|
|
73
73
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testing
|
|
3
3
|
description: |
|
|
4
|
-
|
|
5
|
-
green
|
|
6
|
-
Use when writing or changing tests, or when a suite is flaky, slow or green for the wrong reason.
|
|
4
|
+
Use when writing or changing tests, when asked whether the tests are green after a change, or when a suite
|
|
5
|
+
is flaky, slow, or green for the wrong reason.
|
|
7
6
|
---
|
|
8
7
|
|
|
9
8
|
# Testing Discipline
|
|
@@ -25,6 +24,15 @@ Goal: **behavior correctness** — test real behavior without breaking product c
|
|
|
25
24
|
- **Flaky = bug:** an occasionally failing test is not tolerated; it is fixed at the root.
|
|
26
25
|
- In snapshot/golden-file tests, avoid needless brittleness (assert only the meaningful output).
|
|
27
26
|
|
|
27
|
+
## Excuses that skip a test
|
|
28
|
+
|
|
29
|
+
| Excuse | Reality |
|
|
30
|
+
|---|---|
|
|
31
|
+
| "It is too simple to break." | Simple code breaks at its boundaries; the boundary test is the cheap one. |
|
|
32
|
+
| "I'll add the test after it works." | After it works, the test is written to pass, not to catch. |
|
|
33
|
+
| "I ran it by hand." | A hand check is not repeated on the next change. |
|
|
34
|
+
| "The old test fails, so I updated the expected value." | Find out first whether the code or the expectation is wrong. |
|
|
35
|
+
|
|
28
36
|
## Tests that cannot fail
|
|
29
37
|
|
|
30
38
|
Green is evidence only if the test could have gone red. Shapes that check nothing:
|
|
@@ -34,9 +42,16 @@ Green is evidence only if the test could have gone red. Shapes that check nothin
|
|
|
34
42
|
both sides still matches.
|
|
35
43
|
- **A double asserted against its own stub:** the fake is told to return X and the test asserts X; only the fake runs.
|
|
36
44
|
- **No assertion:** it passes because nothing threw. If *not throwing* is the behavior under test, assert that.
|
|
45
|
+
- **Synthetic input:** the test hands the function a value its real callers never produce, so it passes on a path the
|
|
46
|
+
bug never takes. Reproduce through the entry point the bug came in by (the CLI, the handler, the parser's output).
|
|
37
47
|
|
|
38
48
|
**Prove it can fail:** break what the test covers — invert a condition, return a wrong constant — confirm it goes red,
|
|
39
49
|
then restore. Still green means the test is documentation, not a gate: fix it or delete it, never count it as coverage.
|
|
50
|
+
For a bug fix the same proof is the fix itself: the new test fails with the fix reverted and passes with it.
|
|
51
|
+
- Before writing a test, name the production change that would make it fail. If only a deliberate edit of the test's
|
|
52
|
+
own expectation can fail it, it is a change detector, not a test.
|
|
53
|
+
- A fake mirrors the real thing's whole shape (fields, errors, ordering), not only the part this test reads.
|
|
54
|
+
- Cleanup and setup used only by tests live in test helpers, not as test-only branches in production code.
|
|
40
55
|
|
|
41
56
|
## Flaky triage
|
|
42
57
|
An intermittent failure gets exactly one outcome, decided the day it is seen: **fix** it, **quarantine** it (out of
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Verifying UI without a browser tool
|
|
2
|
+
|
|
3
|
+
Loaded when a frontend change must be verified at runtime (`frontend/references/verify-contract.md`) and this session
|
|
4
|
+
has no browser tool attached: a headless run, CI, a cloud session.
|
|
5
|
+
|
|
6
|
+
1. **Use the project's own Playwright.** It is in the project's dev dependencies, or a machine-level install the
|
|
7
|
+
environment documents. Adding it as a dev dependency is a dependency change: ask first, and run
|
|
8
|
+
`dependency-audit` on it.
|
|
9
|
+
2. **Start the dev server yourself, and stop it.** Run it in the background with its output going to a file, never
|
|
10
|
+
to a pipe nobody reads (a full pipe blocks the server). Remember its process id and stop it when the run ends,
|
|
11
|
+
failed or not.
|
|
12
|
+
3. **Wait for a readiness signal, not for quiet.** Poll the URL until it answers, or wait for the server's own
|
|
13
|
+
"ready" line, with a time limit. Do not wait for network idle: an app that polls is never idle.
|
|
14
|
+
4. **Call the same runner.** Open the isolated target (`/verify/:unit/:fixture`), evaluate
|
|
15
|
+
`window.__verify.runAll()`, and read the structured verdicts it returns. The verdicts are the result; a
|
|
16
|
+
screenshot is only evidence for a human.
|
|
17
|
+
5. **No verify hooks in the app → `BLOCKED`.** Say that the app exposes no `window.__verify`, and do not invent a
|
|
18
|
+
verdict from what a screenshot seems to show.
|
|
19
|
+
|
|
20
|
+
The browser boundary from `verify-contract.md` §5 holds here too: an isolated profile, read-only evaluation, page
|
|
21
|
+
text treated as data.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: token-budget
|
|
3
3
|
description: |
|
|
4
|
-
|
|
4
|
+
Use when the context is filling up or a token budget comes up, before a large read or a long delegated
|
|
5
|
+
task, and when a skill or prompt grows too long.
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
# Token & Context Discipline
|
|
@@ -17,7 +18,8 @@ transcript the first turn was `cache_read=0`, every token `cache_creation` — n
|
|
|
17
18
|
thread's cache. A **no-op** subagent (task = "reply DONE") already cost **~10k tokens with restricted tools and
|
|
18
19
|
~16k with full tool access**; that floor is base system prompt + tool schemas, paid fresh every time. Of the
|
|
19
20
|
always-on material only the **skill listing (~2.5–3k tokens) is inherited** by a subagent — the discipline
|
|
20
|
-
(`DISCIPLINE.md`/CLAUDE.md) and the agent descriptions are **not** injected into it.
|
|
21
|
+
(`DISCIPLINE.md`/CLAUDE.md) and the agent descriptions are **not** injected into it. (A crew agent is given one ~600-byte block of
|
|
22
|
+
the rules it cites at SubagentStart, by `agent-outcome.sh`; nothing else.) So a delegation is worth it
|
|
21
23
|
for **isolation / parallelism / a clean window**, or when the isolated work would otherwise cost the main thread
|
|
22
24
|
**more than that ~10–16k floor** — never by default.
|
|
23
25
|
|
|
@@ -36,16 +38,35 @@ for **isolation / parallelism / a clean window**, or when the isolated work woul
|
|
|
36
38
|
reaching for a built-in is correct — just size the request the way rule 3 sizes any other delegation.
|
|
37
39
|
6. **Least tooling.** An agent holds only the tools it needs; extras accidentally pollute the context + burn the limit.
|
|
38
40
|
7. **Lean SKILL.md.** Skills load into the main context; heavy reference goes to a separate file, only when needed.
|
|
39
|
-
8. **Targeted reading.** Instead of reading a whole file, pinpoint with Grep/Glob.
|
|
41
|
+
8. **Targeted reading.** Instead of reading a whole file, pinpoint with Grep/Glob. Index first, then fetch: list or
|
|
42
|
+
count (`grep -l`, `grep -c`, `git diff --stat`, the headings), then read only the hits. A `file:line` you were
|
|
43
|
+
handed (crew-map, a task card, a finding) is where to open the file with offset/limit, not a reason to read all of
|
|
44
|
+
it or search for it again. After a compaction, file content still in context (the summary, a kept tool output) is
|
|
45
|
+
a record of that moment: read the file again before editing it.
|
|
40
46
|
9. **Measure fill, don't guess.** `bash .claude/hooks/context-usage.sh` (Bash tool, not PowerShell) reads it from the transcript; crew-session-manager recommends continue or handoff+clear; the user runs `/clear` at a phase boundary.
|
|
41
47
|
10. **Bound what a command hands back.** All of the rules above govern the context's own footprint; none of them
|
|
42
48
|
govern what a single `Bash` call dumps into it. A `find` over a monorepo, an unfiltered log, a full test run —
|
|
43
49
|
each returns everything to the main thread whether or not any of it is read. Ask for the answer, not the
|
|
44
50
|
corpus: `grep -c` over `grep`, `| tail -20` over the whole file, `--quiet`/`--porcelain` where the tool has
|
|
45
|
-
one, and a redirect to a file plus a pointer when the output is genuinely needed later (rule 2).
|
|
51
|
+
one, and a redirect to a file plus a pointer when the output is genuinely needed later (rule 2). When the first
|
|
52
|
+
bounded look was not enough, redirect the full output to a file and grep it; never re-run the command with a
|
|
53
|
+
bigger head/tail, which pays for the same output again and again.
|
|
54
|
+
**A test or build run keeps its own status.** `npm test | tail -20` exits with tail's status, which is 0 when the
|
|
55
|
+
suite failed. Use `set -o pipefail` before the pipe, or `cmd > .crewforth/run.log 2>&1; echo "exit=$?"; tail -20
|
|
56
|
+
.crewforth/run.log`. An exit code read through a pipe without pipefail is the pipe's, not the suite's.
|
|
46
57
|
11. **Cut what nothing reaches — with evidence, not a hunch.** Every installed skill spends its name and
|
|
47
58
|
description in EVERY session, forever. `bash .claude/eval/utilization.sh` (Bash tool, not PowerShell; a file install only) reports which skills actually fired
|
|
48
59
|
in this project's transcripts and how many bytes the cold ones cost, which is the list `doctor.sh` §4a's
|
|
49
60
|
`skillOverrides: name-only` advice needs and never had. Read it as evidence, not a verdict: a skill that only
|
|
50
61
|
fires during an incident is doing its job by existing. `--all-projects` widens the scope; by default it reads
|
|
51
|
-
this project only and prints names and counts, never paths or content.
|
|
62
|
+
this project only and prints names and counts, never paths or content. `--tools` does the same for tools and
|
|
63
|
+
MCP servers: available in five or more sessions and never called, printed as a deny proposal you apply yourself.
|
|
64
|
+
12. **A dispatch carries paths, not history.** The prompt of an `Agent` call names the files, the task card and, when
|
|
65
|
+
there is more to say, a brief file to read; it never pastes the conversation, a long diff or earlier tool output.
|
|
66
|
+
The subagent re-pays every byte of it (the floor above) and reads what it needs itself.
|
|
67
|
+
13. **Trimming keeps the constraints.** When you shorten or summarise text (a handoff, a brief, a finding), every
|
|
68
|
+
sentence that carries a must, a cannot, a limit or a number survives; the colour around it is what goes.
|
|
69
|
+
14. **A loaded tool's schema is paid on every request,** used or not. A deferred tool (tool search) costs only its
|
|
70
|
+
name until it is loaded. How much a server costs depends on its schemas and is not measured by Crewforth.
|
|
71
|
+
`utilization.sh --tools` (rule 11) sees only the deferred ones, the cheap case; a loaded tool nobody calls has to
|
|
72
|
+
be found in the settings that load it.
|
|
@@ -26,11 +26,18 @@ tree (with your in-progress edits) is physically untouched. Throw the worktree a
|
|
|
26
26
|
- Skip it for a single, in-place edit on a clean tree — a worktree is overhead there.
|
|
27
27
|
|
|
28
28
|
## How
|
|
29
|
-
1. **Detect** — are you already in a worktree? `[ "$(git rev-parse --git-dir)" != "$(git rev-parse --git-common-dir)" ]` is true inside one; don't nest.
|
|
30
|
-
2. **Create** — `git worktree add ../wt-<name> -b <branch>` (new branch) or `git worktree add ../wt-<name> <existing>`. It shares the object store, so it's cheap.
|
|
31
|
-
3. **Work there** — cd into it; edits, builds, and agent runs stay local to that copy.
|
|
32
|
-
4. **Fold back or discard** — keep: commit on its branch, then merge. Discard: `git worktree remove ../wt-<name>`
|
|
33
|
-
5. **Clean up** — `git worktree prune` removes stale entries. Never leave orphaned worktrees around.
|
|
29
|
+
1. **Detect** — are you already in a worktree? `[ "$(git rev-parse --git-dir)" != "$(git rev-parse --git-common-dir)" ]` is true inside one; don't nest. A submodule answers true as well: when `git rev-parse --show-superproject-working-tree` prints a path, you are in a submodule, not a worktree.
|
|
30
|
+
2. **Create** — `git worktree add ../wt-<name> -b <branch>` (new branch) or `git worktree add ../wt-<name> <existing>`. It shares the object store, so it's cheap. Before `-b <branch>`, check `git show-ref --verify --quiet refs/heads/<branch>`: if the branch exists, do not reuse the name silently; ask whether to work on that existing branch. A branch you adopted that way is the user's, and cleanup never deletes it.
|
|
31
|
+
3. **Work there** — cd into it; edits, builds, and agent runs stay local to that copy. Run the project's test command once before the first edit and note the result, so a failure you see later is yours or was already there. `.claude/` is gitignored in most installs, so a worktree made with `git worktree add` has none: entering it with the EnterWorktree tool copies it in, and `/crew-doctor` names a worktree that still lacks it.
|
|
32
|
+
4. **Fold back or discard** — keep: commit on its branch, then merge. Discard: first look at what would be lost, `git -C ../wt-<name> status --porcelain` and `git log <upstream-or-base>..<branch> --oneline`. Uncommitted files or commits that exist nowhere else mean you list them and ask the user; `git worktree remove ../wt-<name>` only when both are empty, and `--force` only after the user has seen that list and said to drop it.
|
|
33
|
+
5. **Clean up** — `git worktree prune` removes stale entries. Never leave orphaned worktrees around. Delete only a branch you created for the worktree (`git branch -d`, which refuses an unmerged one), never one you adopted in step 2.
|
|
34
|
+
|
|
35
|
+
## Shared services
|
|
36
|
+
A database, dev server or queue the worktrees all use is shared state, not part of your copy.
|
|
37
|
+
- Reuse a service that is already up and healthy; start one only when none answers.
|
|
38
|
+
- Never stop or kill a process you did not start. Two worktrees share one machine, so the other one may be using it.
|
|
39
|
+
- Read the port the tool reports when it starts; don't assume the default, since the other worktree may already hold it.
|
|
40
|
+
- A test that destroys data (drops tables, clears a queue) gets its own database or namespace, never the shared one.
|
|
34
41
|
|
|
35
42
|
## Invariant rules
|
|
36
43
|
1. **Isolate before risk, don't revert after** — a worktree replaces "run `checkout -- .` to undo"; the undo is `worktree remove`.
|
|
@@ -38,3 +45,4 @@ tree (with your in-progress edits) is physically untouched. Throw the worktree a
|
|
|
38
45
|
3. **Don't nest** — creating a worktree from inside a worktree; detect first.
|
|
39
46
|
4. **Commit or isolate before delegating file-mutating work** — a subagent must never be able to discard the main tree's unsaved changes.
|
|
40
47
|
5. **Remove what you add** — no orphaned worktrees; `remove` + `prune` when done.
|
|
48
|
+
6. **Never remove what you have not looked at** — dirty files or unpushed commits in a worktree are the user's to drop, not yours; no `--force` and no `branch -D` without their word.
|
|
@@ -17,16 +17,19 @@ import fs from 'node:fs';
|
|
|
17
17
|
|
|
18
18
|
import { prepare, pending, watch, cleanup, alwaysList, logApprovals, writeMode } from './permissions.js';
|
|
19
19
|
import { getFleet } from './fleet.js';
|
|
20
|
+
import { headOf, readChanges } from './worktree-changes.js';
|
|
20
21
|
|
|
21
22
|
export const ALLOWED_MODES = ['plan', 'acceptEdits', 'default'];
|
|
22
23
|
const DEFAULT_MODE = 'plan';
|
|
23
24
|
const MAX_EVENTS = 4000; // ring buffer per session
|
|
24
25
|
const MAX_MESSAGE_BYTES = 512 * 1024;
|
|
26
|
+
// What the session changed is read again at most this often: the panel's own session poll (app.js SESSION_POLL_MS).
|
|
27
|
+
const CHANGES_EVERY_MS = 5000;
|
|
25
28
|
|
|
26
29
|
const sessions = new Map(); // sessionId -> OwnedSession
|
|
27
30
|
|
|
28
31
|
class OwnedSession {
|
|
29
|
-
constructor({ cwd, model, permissionMode, sessionId, resume, forked = false }) {
|
|
32
|
+
constructor({ cwd, model, permissionMode, sessionId, resume, forked = false, startHead = null }) {
|
|
30
33
|
this.id = sessionId;
|
|
31
34
|
this.resumedFrom = resume ?? null;
|
|
32
35
|
// Whether this is a copy or the session itself. A copy is not a defeat but
|
|
@@ -45,6 +48,11 @@ class OwnedSession {
|
|
|
45
48
|
this.lastError = null;
|
|
46
49
|
this.turns = 0;
|
|
47
50
|
this.costUsd = 0;
|
|
51
|
+
// The commit the directory was on before the child existed, and what has changed since (in memory only).
|
|
52
|
+
this.startHead = startHead;
|
|
53
|
+
this.changes = startHead ? null : { measured: false, reason: 'not a git repository with a commit' };
|
|
54
|
+
this.changesAt = 0;
|
|
55
|
+
this.changesBusy = false;
|
|
48
56
|
|
|
49
57
|
// The gate is prepared before the child exists, so its very first tool call
|
|
50
58
|
// is already covered. A missing hook is reported, never worked around: a
|
|
@@ -252,7 +260,17 @@ class OwnedSession {
|
|
|
252
260
|
return { ok: true };
|
|
253
261
|
}
|
|
254
262
|
|
|
263
|
+
/** Read the changes again when the last read is old enough and none is running. The answer lands on the next poll. */
|
|
264
|
+
refreshChanges(now = Date.now()) {
|
|
265
|
+
if (!this.startHead || this.changesBusy || now - this.changesAt < CHANGES_EVERY_MS) return null;
|
|
266
|
+
this.changesBusy = true; this.changesAt = now;
|
|
267
|
+
return readChanges(this.cwd, this.startHead)
|
|
268
|
+
.then((c) => { this.changes = c; return c; })
|
|
269
|
+
.finally(() => { this.changesBusy = false; });
|
|
270
|
+
}
|
|
271
|
+
|
|
255
272
|
summary() {
|
|
273
|
+
this.refreshChanges();
|
|
256
274
|
return {
|
|
257
275
|
sessionId: this.id,
|
|
258
276
|
cwd: this.cwd,
|
|
@@ -279,6 +297,8 @@ class OwnedSession {
|
|
|
279
297
|
// a viewer on another machine has a different one.
|
|
280
298
|
now: Date.now(),
|
|
281
299
|
gateEvents: this.gateEvents.slice(-40),
|
|
300
|
+
// Files changed since the session started, against the commit recorded then; null until the first read lands.
|
|
301
|
+
changes: this.changes,
|
|
282
302
|
};
|
|
283
303
|
}
|
|
284
304
|
}
|
|
@@ -323,8 +343,9 @@ export async function createSession({ cwd, model, permissionMode, resume } = {})
|
|
|
323
343
|
if (sessions.has(sessionId)) {
|
|
324
344
|
return { ok: false, reason: 'the panel is already running that session' };
|
|
325
345
|
}
|
|
346
|
+
const startHead = await headOf(dir);
|
|
326
347
|
const s = new OwnedSession({
|
|
327
|
-
cwd: dir, model: model || null, permissionMode: mode, sessionId, resume: resume || null, forked,
|
|
348
|
+
cwd: dir, model: model || null, permissionMode: mode, sessionId, resume: resume || null, forked, startHead,
|
|
328
349
|
});
|
|
329
350
|
sessions.set(sessionId, s);
|
|
330
351
|
return { ok: true, session: s };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// What a session started here has changed in its directory, read from git.
|
|
2
|
+
//
|
|
3
|
+
// The commit the directory was on when the session started is recorded first; after that, the working tree is
|
|
4
|
+
// compared with it: `git diff-index --numstat <start>` for tracked files (what the session committed since counts too),
|
|
5
|
+
// and `git ls-files -o --exclude-standard` for new files git does not track yet. Nothing is staged to make a new
|
|
6
|
+
// file visible (no `add -N`), and the result is kept in memory only: it is never written anywhere.
|
|
7
|
+
//
|
|
8
|
+
// READ-ONLY. The plumbing `diff-index`, not `git diff`: measured on git 2.43, `git diff <commit>` rewrote the index
|
|
9
|
+
// to refresh a file whose time had changed, GIT_OPTIONAL_LOCKS=0 and --no-optional-locks notwithstanding (both are
|
|
10
|
+
// kept for `status`-like calls a later edit may add); `diff-index` reads the stat data and writes nothing. With
|
|
11
|
+
// `core.fsmonitor=` empty, a repository's own config cannot name a program for these calls to run; the diff runs
|
|
12
|
+
// with --no-ext-diff --no-textconv for the same reason.
|
|
13
|
+
//
|
|
14
|
+
// NOT MEASURED is said, not shown as zero: a directory that is not a repository, a git that is missing, or a call
|
|
15
|
+
// that fails gives `{ measured: false, reason }`.
|
|
16
|
+
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
const GIT_TIMEOUT_MS = 5000;
|
|
20
|
+
const MAX_BUFFER = 8 * 1024 * 1024;
|
|
21
|
+
const SAFE = ['-c', 'core.fsmonitor=', '-c', 'core.quotePath=false', '--no-optional-locks'];
|
|
22
|
+
|
|
23
|
+
function git(cwd, args) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
execFile('git', [...SAFE, ...args], {
|
|
26
|
+
cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: MAX_BUFFER, windowsHide: true,
|
|
27
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', LC_ALL: 'C' },
|
|
28
|
+
}, (err, stdout) => resolve(err ? null : String(stdout)));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The commit `cwd` is on, or null (not a repository, no commit yet, no git). */
|
|
33
|
+
export async function headOf(cwd) {
|
|
34
|
+
const out = await git(cwd, ['rev-parse', '--verify', '-q', 'HEAD^{commit}']);
|
|
35
|
+
const sha = out?.trim();
|
|
36
|
+
return sha && /^[0-9a-f]{40,64}$/.test(sha) ? sha : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Parse `git diff-index --numstat -z --no-renames`: "added\tremoved\tpath\0" per file, "-" for a binary file's counts. */
|
|
40
|
+
export function parseNumstat(text) {
|
|
41
|
+
let files = 0; let added = 0; let removed = 0; let binary = 0;
|
|
42
|
+
for (const rec of String(text ?? '').split('\0')) {
|
|
43
|
+
const m = /^(-|\d+)\t(-|\d+)\t./s.exec(rec);
|
|
44
|
+
if (!m) continue;
|
|
45
|
+
files += 1;
|
|
46
|
+
if (m[1] === '-' || m[2] === '-') { binary += 1; continue; }
|
|
47
|
+
added += Number(m[1]); removed += Number(m[2]);
|
|
48
|
+
}
|
|
49
|
+
return { files, added, removed, binary };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Changes in `cwd` since `base` (a commit id). */
|
|
53
|
+
export async function readChanges(cwd, base) {
|
|
54
|
+
if (!base) return { measured: false, reason: 'no commit was recorded when the session started' };
|
|
55
|
+
const [diff, other] = await Promise.all([
|
|
56
|
+
git(cwd, ['diff-index', '--numstat', '-z', '--no-renames', '--no-ext-diff', '--no-textconv', base, '--']),
|
|
57
|
+
git(cwd, ['ls-files', '-o', '--exclude-standard', '-z']),
|
|
58
|
+
]);
|
|
59
|
+
if (diff === null || other === null) return { measured: false, reason: 'git could not compare this directory' };
|
|
60
|
+
const n = parseNumstat(diff);
|
|
61
|
+
const untracked = other.split('\0').filter(Boolean).length;
|
|
62
|
+
return { measured: true, base, ...n, untracked };
|
|
63
|
+
}
|
|
64
|
+
|
package/kit/studio/web/convo.js
CHANGED
|
@@ -148,6 +148,8 @@ export function headerOf(session, { readOnly = false, contextTokens = null } = {
|
|
|
148
148
|
if (typeof session?.turns === 'number') facts.push(`${session.turns} ${session.turns === 1 ? 'turn' : 'turns'}`);
|
|
149
149
|
if (contextTokens != null) facts.push(`${fmtK(contextTokens)} ctx`);
|
|
150
150
|
if (facts.length) parts.push(facts.join(' · '));
|
|
151
|
+
const changed = changesLine(session?.changes);
|
|
152
|
+
if (changed) parts.push(changed);
|
|
151
153
|
return {
|
|
152
154
|
badge: 'Started here',
|
|
153
155
|
tone: 'good',
|
|
@@ -159,6 +161,18 @@ export function headerOf(session, { readOnly = false, contextTokens = null } = {
|
|
|
159
161
|
};
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
/**
|
|
165
|
+
* What a session started here has changed since it started, as the server read it from git: null when it was not
|
|
166
|
+
* measured or nothing changed, so a strip never shows a 0 nobody counted.
|
|
167
|
+
*/
|
|
168
|
+
export function changesLine(c) {
|
|
169
|
+
if (!c?.measured || (!c.files && !c.untracked)) return null;
|
|
170
|
+
const bits = [];
|
|
171
|
+
if (c.files) bits.push(`${c.files} ${c.files === 1 ? 'file' : 'files'} changed (+${c.added} −${c.removed})`);
|
|
172
|
+
if (c.untracked) bits.push(`${c.untracked} new untracked`);
|
|
173
|
+
return bits.join(' · ');
|
|
174
|
+
}
|
|
175
|
+
|
|
162
176
|
function fmtK(t) {
|
|
163
177
|
if (t < 1000) return `${t}`;
|
|
164
178
|
if (t < 1_000_000) return `${(t / 1000).toFixed(t < 10_000 ? 1 : 0)}k`;
|