compounding-loop 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +93 -3
- package/bin/loop.js +2 -0
- package/dist/cli/dashboard.js +52 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/gh.js +29 -0
- package/dist/cli/gh.js.map +1 -0
- package/dist/cli/index.js +144 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.js +72 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/loops.js +72 -0
- package/dist/cli/loops.js.map +1 -0
- package/dist/cli/new.js +54 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/run.js +81 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/core/blocked.js +42 -0
- package/dist/core/blocked.js.map +1 -0
- package/dist/core/brief.js +91 -0
- package/dist/core/brief.js.map +1 -0
- package/dist/core/gh.js +2 -0
- package/dist/core/gh.js.map +1 -0
- package/dist/core/init.js +59 -0
- package/dist/core/init.js.map +1 -0
- package/dist/core/repo.js +81 -0
- package/dist/core/repo.js.map +1 -0
- package/dist/core/rounds.js +50 -0
- package/dist/core/rounds.js.map +1 -0
- package/dist/core/runPrompt.js +31 -0
- package/dist/core/runPrompt.js.map +1 -0
- package/dist/core/status.js +21 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/table.js +24 -0
- package/dist/core/table.js.map +1 -0
- package/dist/core/template.js +29 -0
- package/dist/core/template.js.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/server/data.js +36 -0
- package/dist/server/data.js.map +1 -0
- package/dist/server/events.js +105 -0
- package/dist/server/events.js.map +1 -0
- package/dist/server/five-sites.json +3071 -0
- package/dist/server/health.js +133 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.js +134 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/loops.js +142 -0
- package/dist/server/loops.js.map +1 -0
- package/dist/server/settings.js +25 -0
- package/dist/server/settings.js.map +1 -0
- package/dist/server/static-export.js +28 -0
- package/dist/server/static-export.js.map +1 -0
- package/dist/server/static.js +80 -0
- package/dist/server/static.js.map +1 -0
- package/dist/web/assets/index-BDdtnG33.js +49 -0
- package/dist/web/assets/index-D3-N8DN4.css +2 -0
- package/dist/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.svg +1 -0
- package/method/COMPOUNDING_LOOP.md +569 -0
- package/method/example/K-0001.md +16 -0
- package/method/knowledge-index.md +5 -0
- package/method/operating-card.md +40 -0
- package/method/retro.md +10 -0
- package/method/reviewer-prompt.md +13 -0
- package/method/stuck-protocol.md +12 -0
- package/method/task-template.md +41 -0
- package/package.json +71 -4
- package/plugin/.claude-plugin/plugin.json +7 -0
- package/plugin/commands/handoff.md +10 -0
- package/plugin/commands/loop-init.md +13 -0
- package/plugin/commands/retro.md +11 -0
- package/plugin/commands/stuck.md +10 -0
- package/runners/github-actions/loop.yml +51 -0
- package/runners/local/README.md +8 -0
- package/runners/local/crontab.txt +6 -0
- package/runners/local/task-scheduler.xml +32 -0
- package/runners/routine/README.md +19 -0
- package/runners/routine/prompt.md +13 -0
- package/templates/chrome-extension/CLAUDE.md +18 -0
- package/templates/chrome-extension/IDEA.md +22 -0
- package/templates/chrome-extension/extension/background.js +5 -0
- package/templates/chrome-extension/extension/manifest.json +9 -0
- package/templates/chrome-extension/extension/popup.css +3 -0
- package/templates/chrome-extension/extension/popup.html +15 -0
- package/templates/chrome-extension/extension/popup.js +16 -0
- package/templates/chrome-extension/gitignore +6 -0
- package/templates/chrome-extension/package.json +12 -0
- package/templates/chrome-extension/playwright.config.js +22 -0
- package/templates/chrome-extension/scripts/check.mjs +51 -0
- package/templates/chrome-extension/scripts/test.mjs +59 -0
- package/templates/chrome-extension/tests/popup.spec.js +37 -0
- package/templates/static-site/.ai/index.md +5 -0
- package/templates/static-site/.ai/log.md +3 -0
- package/templates/static-site/.ai/loop-prompt.md +10 -0
- package/templates/static-site/.ai/task.md +27 -0
- package/templates/static-site/.gitattributes +1 -0
- package/templates/static-site/CLAUDE.md +113 -0
- package/templates/static-site/IDEA.md +36 -0
- package/templates/static-site/gitignore +6 -0
- package/templates/static-site/netlify.toml +14 -0
- package/templates/static-site/package.json +14 -0
- package/templates/static-site/playwright.config.js +27 -0
- package/templates/static-site/scripts/check.mjs +188 -0
- package/templates/static-site/scripts/serve.mjs +44 -0
- package/templates/static-site/scripts/test.mjs +59 -0
- package/templates/static-site/site/index.html +23 -0
- package/templates/static-site/site/robots.txt +4 -0
- package/templates/static-site/site/sitemap.xml +4 -0
- package/templates/static-site/tests/pages.spec.js +21 -0
- package/templates/static-site/vercel.json +19 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Stuck protocol
|
|
2
|
+
|
|
3
|
+
Triggered by the same failure twice, or by two runs with no progress toward done-when. Do not retry a variant.
|
|
4
|
+
|
|
5
|
+
1. Stop. Record the attempt under Tried: what you did, the exact error, why it failed.
|
|
6
|
+
2. Search `.ai/index.md` for the error text.
|
|
7
|
+
3. Write down every assumption you are making. Move the ones you never tested to Guesses.
|
|
8
|
+
4. Test the weakest assumption with the cheapest experiment.
|
|
9
|
+
5. Try a structurally different approach, not a variant of what failed.
|
|
10
|
+
6. Still stuck, unattended: write `BLOCKED.md` (what you tried, the specific question, your best guess), commit, push and stop. With a human present: ask the same question.
|
|
11
|
+
|
|
12
|
+
Hard cap: a task that reaches about 5 runs without the checks passing goes to `BLOCKED.md`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# TASK: <name>
|
|
2
|
+
|
|
3
|
+
Run: 0 / 30
|
|
4
|
+
Status: not started
|
|
5
|
+
|
|
6
|
+
## Contract
|
|
7
|
+
**Goal:** build v1 of IDEA.md (replace with one sentence per task)
|
|
8
|
+
**Done when:** derive from IDEA.md: `<command>` passes; <other checks>
|
|
9
|
+
**Constraints:** <what must not change, what must be reused>
|
|
10
|
+
**Out of scope:** <what not to touch>
|
|
11
|
+
|
|
12
|
+
## Current state
|
|
13
|
+
(rewrite every window)
|
|
14
|
+
- Branch / commit: <branch> @ <hash>, clean
|
|
15
|
+
- Checks: <passing / failing, names>
|
|
16
|
+
- Next step: <exactly one step>
|
|
17
|
+
- Read first: <path:lines>, <K-ids>
|
|
18
|
+
|
|
19
|
+
## Confirmed
|
|
20
|
+
(facts proven by a command or test, with how we know)
|
|
21
|
+
- <fact> (seen in <test / log / commit>)
|
|
22
|
+
|
|
23
|
+
## Guesses
|
|
24
|
+
(unproven; test before building on them, never treat as fact in a later run)
|
|
25
|
+
- <hypothesis> (test: <cheapest experiment>)
|
|
26
|
+
|
|
27
|
+
## Tried
|
|
28
|
+
(append-only: approach → result → why)
|
|
29
|
+
1. <approach> → <result> → <why it failed>
|
|
30
|
+
|
|
31
|
+
## Decisions
|
|
32
|
+
(decision · reason · how to reverse)
|
|
33
|
+
|
|
34
|
+
## Don't
|
|
35
|
+
- <thing not to retry> (Tried #<n>)
|
|
36
|
+
|
|
37
|
+
## Handoff
|
|
38
|
+
(each run ends by writing: where it stopped, the exact next step, anything half-done)
|
|
39
|
+
|
|
40
|
+
## promote?
|
|
41
|
+
(for the retro: general lessons worth keeping)
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compounding-loop",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Write the brief, check back tomorrow: unattended, verified Claude Code build loops with a local dashboard.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"loop": "bin/loop.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"dist/",
|
|
13
|
+
"templates/",
|
|
14
|
+
"method/",
|
|
15
|
+
"runners/",
|
|
16
|
+
"plugin/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Minntooos/compounding-loop.git"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"claude-code",
|
|
29
|
+
"ai-agents",
|
|
30
|
+
"autonomous",
|
|
31
|
+
"dashboard",
|
|
32
|
+
"cli",
|
|
33
|
+
"context-engineering"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"check": "node scripts/check.mjs",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p web/tsconfig.json --noEmit",
|
|
38
|
+
"unit": "vitest run",
|
|
39
|
+
"build": "tsc -p tsconfig.build.json && node demo/copy-demo.mjs && vite build",
|
|
40
|
+
"e2e": "playwright test",
|
|
41
|
+
"test": "npm run check && npm run typecheck && npm run unit && npm run build && npm run e2e",
|
|
42
|
+
"dev:web": "vite",
|
|
43
|
+
"preview": "vite preview --port 4173 --strictPort"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@fontsource-variable/inter": "^5.3.0",
|
|
47
|
+
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
|
48
|
+
"@playwright/test": "^1.64.0",
|
|
49
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
50
|
+
"@tanstack/react-query": "^5.104.1",
|
|
51
|
+
"@types/node": "^26.6.4",
|
|
52
|
+
"@types/react": "^19.3.0",
|
|
53
|
+
"@types/react-dom": "^19.3.0",
|
|
54
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
55
|
+
"cmdk": "^1.1.1",
|
|
56
|
+
"lucide-react": "^1.52.0",
|
|
57
|
+
"react": "^19.3.0",
|
|
58
|
+
"react-dom": "^19.3.0",
|
|
59
|
+
"recharts": "^3.10.1",
|
|
60
|
+
"tailwindcss": "^4.3.3",
|
|
61
|
+
"tsx": "^4.23.15",
|
|
62
|
+
"typescript": "^7.0.2",
|
|
63
|
+
"vite": "^8.3.3",
|
|
64
|
+
"vitest": "^5.0.3",
|
|
65
|
+
"yaml": "^2.9.1"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@clack/prompts": "^1.8.1",
|
|
69
|
+
"@hono/node-server": "^2.1.4",
|
|
70
|
+
"commander": "^15.0.0",
|
|
71
|
+
"hono": "^4.13.13"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "compounding-loop",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run unattended, verified Claude Code build loops: set up a repo, hand off between windows, run retros and escape stuck loops.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": ["loop", "unattended", "verification", "handoff", "retro"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Write the handoff so a fresh session with zero memory can continue at full quality
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Run the handoff protocol (method section 9.1) for the current task.
|
|
6
|
+
|
|
7
|
+
1. Run the checks and record the real result (counts and failing names) in `.ai/task.md` under "Current state".
|
|
8
|
+
2. Rewrite "Current state": branch and commit, whether the tree is clean, the single next step, and what to read first (`path:line`, knowledge ids). Do not copy code into the file.
|
|
9
|
+
3. Make sure every new fact is under Confirmed (with how we know) or Guesses (with the cheapest test). Move anything half-done into the handoff as "half-done: ...".
|
|
10
|
+
4. Commit the task file. Then tell the user the next window is ready and name the first thing it will do.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Set up this repository for the Compounding Loop (operating card, task file, knowledge index)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Set this repository up for unattended, verified build loops.
|
|
6
|
+
|
|
7
|
+
1. If the `loop` CLI is available, run `npx compounding-loop init` and report what it created. Otherwise do the steps below by hand.
|
|
8
|
+
2. Make sure these exist, creating only what is missing and never overwriting the user's files:
|
|
9
|
+
- `CLAUDE.md` containing the Operating Card (the "Operating Card (the Compounding Loop)" section).
|
|
10
|
+
- `.ai/method.md`, `.ai/index.md`, `.ai/knowledge/`, `.ai/log.md`.
|
|
11
|
+
- `.ai/task.md` with `Run: 0 / 30`, a Contract with a "Done when" that a command can check, and empty Confirmed, Guesses, Tried and Decisions sections.
|
|
12
|
+
3. Ask the user for the one-sentence goal and the command that proves it works (for example `npm test`) if `IDEA.md` or the task contract does not already say.
|
|
13
|
+
4. Finish with the exact command the user should run next, and do not start the build yourself.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the retro after the checks pass - promote lessons, flag stale entries, log the task
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Run the retro (method section 12) now that the checks pass.
|
|
6
|
+
|
|
7
|
+
1. Run the checks once more and confirm they pass. If they do not, stop and say so; the task is not done.
|
|
8
|
+
2. Read `.ai/task.md`. List the lessons worth keeping beyond this task (Confirmed facts that will bite again, gotchas, decisions). For each, propose a knowledge entry with its triggers (files, error text, commands, topics) and evidence.
|
|
9
|
+
3. Flag existing entries in `.ai/index.md` that this task showed to be wrong or stale. Do not delete or rewrite a CONFIRMED entry without new evidence.
|
|
10
|
+
4. Add one line to `.ai/log.md`: date, task, checks result, number of windows, what was learned.
|
|
11
|
+
5. After the user approves the proposed entries, write them, update `.ai/index.md`, and delete `.ai/task.md`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the stuck protocol when the same failure has happened twice
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
You have hit the same failure twice. Stop retrying variants and run the stuck protocol (method section 9.3).
|
|
6
|
+
|
|
7
|
+
1. Write the failure down under Tried in `.ai/task.md`: what you did, the exact error, and why you think it failed. Search `.ai/index.md` for the error text.
|
|
8
|
+
2. List what you have not yet checked: assumptions you treated as fact (move them to Guesses), the cheapest experiment that would split the possible causes, and whether a subagent could read the logs or docs for you.
|
|
9
|
+
3. Run that one experiment. If it explains the failure, fix it and record the cause as Confirmed.
|
|
10
|
+
4. If it does not, and unattended: write `BLOCKED.md` (what you tried, the specific question, your best guess), commit, push and stop. If a human is present: report the question and your best guess, and wait.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Compounding Loop: scheduled build run on GitHub Actions.
|
|
2
|
+
#
|
|
3
|
+
# Setup (once):
|
|
4
|
+
# 1. Add ONE repository secret under Settings > Secrets and variables > Actions:
|
|
5
|
+
# ANTHROPIC_API_KEY (API billing), or
|
|
6
|
+
# CLAUDE_CODE_OAUTH_TOKEN (a Claude subscription; create it with `claude setup-token`).
|
|
7
|
+
# Never write a key into this file or the repository.
|
|
8
|
+
# 2. Copy this file to .github/workflows/loop.yml.
|
|
9
|
+
# 3. Pick a staggered minute in `cron` below (avoid minute 0).
|
|
10
|
+
name: compounding-loop
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
schedule:
|
|
14
|
+
- cron: "17 * * * *"
|
|
15
|
+
workflow_dispatch: {}
|
|
16
|
+
|
|
17
|
+
# Two runs on one branch would fight; queue instead of cancelling a run mid-push.
|
|
18
|
+
concurrency:
|
|
19
|
+
group: compounding-loop
|
|
20
|
+
cancel-in-progress: false
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: write
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
run:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
timeout-minutes: 90
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
ref: main
|
|
33
|
+
fetch-depth: 0
|
|
34
|
+
- uses: actions/setup-node@v4
|
|
35
|
+
with:
|
|
36
|
+
node-version: 22
|
|
37
|
+
- uses: anthropics/claude-code-action@v1
|
|
38
|
+
with:
|
|
39
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
40
|
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
41
|
+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
42
|
+
prompt: |
|
|
43
|
+
You are one scheduled run of the build loop for this repository. Nobody is watching.
|
|
44
|
+
Read CLAUDE.md and follow "Unattended mode" exactly. IDEA.md is the brief.
|
|
45
|
+
If .ai/session.lock is less than 90 minutes old, or DONE.md or BLOCKED.md exists, reply "Nothing to do" and stop.
|
|
46
|
+
Add 1 to the Run counter in .ai/task.md (Run: N / 30); at 30 write BLOCKED.md ("run budget reached") and stop.
|
|
47
|
+
Do units of work, run the tests after each, and push to main only when they pass.
|
|
48
|
+
Finish with the handoff in .ai/task.md. Never publish, spend money, or add secrets or tracking.
|
|
49
|
+
claude_args: >-
|
|
50
|
+
--max-turns 200
|
|
51
|
+
--allowedTools "Bash,Edit,Write,Read,Glob,Grep,Task"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Local runner
|
|
2
|
+
|
|
3
|
+
Runs the loop from your own machine on a schedule. Needs Node 20+, git and the `claude` CLI signed in to your account.
|
|
4
|
+
|
|
5
|
+
- **macOS / Linux:** paste the line from `crontab.txt` into `crontab -e` and set your repository path.
|
|
6
|
+
- **Windows:** edit the path in `task-scheduler.xml`, then import it with `schtasks /Create /XML task-scheduler.xml /TN CompoundingLoop`. The `claude.exe` native build must be on your PATH. Add `--skip-permissions` to the task's arguments only in a repository you trust, so the unattended run can edit files.
|
|
7
|
+
|
|
8
|
+
Both use the minute `17`; change it so several loops do not start together. Each run is capped by the session lock (90 minutes) and the run budget (`Run: N / 30`).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Compounding Loop on macOS or Linux: run one round at minute 17 of every hour.
|
|
2
|
+
# Install with `crontab -e` and paste the line below, replacing /path/to/repo.
|
|
3
|
+
# `loop run` skips the round when DONE.md / BLOCKED.md exists; the prompt itself checks the session lock.
|
|
4
|
+
# Cron has a minimal PATH: if `loop` or `claude` is not found, use their full paths. For unattended
|
|
5
|
+
# edits add `--skip-permissions` (see `loop run --help`) only in a repository you trust.
|
|
6
|
+
17 * * * * cd /path/to/repo && loop run >> .ai/loop-cron.log 2>&1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<?xml version="1.0"?>
|
|
2
|
+
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
3
|
+
<RegistrationInfo>
|
|
4
|
+
<Description>Compounding Loop: run one round of the build loop every hour.</Description>
|
|
5
|
+
</RegistrationInfo>
|
|
6
|
+
<Triggers>
|
|
7
|
+
<CalendarTrigger>
|
|
8
|
+
<Repetition>
|
|
9
|
+
<Interval>PT1H</Interval>
|
|
10
|
+
<StopAtDurationEnd>false</StopAtDurationEnd>
|
|
11
|
+
</Repetition>
|
|
12
|
+
<StartBoundary>2026-01-01T00:17:00</StartBoundary>
|
|
13
|
+
<Enabled>true</Enabled>
|
|
14
|
+
<ScheduleByDay>
|
|
15
|
+
<DaysInterval>1</DaysInterval>
|
|
16
|
+
</ScheduleByDay>
|
|
17
|
+
</CalendarTrigger>
|
|
18
|
+
</Triggers>
|
|
19
|
+
<Settings>
|
|
20
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
21
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
22
|
+
<ExecutionTimeLimit>PT90M</ExecutionTimeLimit>
|
|
23
|
+
<Enabled>true</Enabled>
|
|
24
|
+
</Settings>
|
|
25
|
+
<Actions Context="Author">
|
|
26
|
+
<Exec>
|
|
27
|
+
<Command>loop.cmd</Command>
|
|
28
|
+
<Arguments>run</Arguments>
|
|
29
|
+
<WorkingDirectory>C:\path\to\repo</WorkingDirectory>
|
|
30
|
+
</Exec>
|
|
31
|
+
</Actions>
|
|
32
|
+
</Task>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Claude Code routine runner
|
|
2
|
+
|
|
3
|
+
A routine is a scheduled Claude Code session in the cloud. It runs `prompt.md` against your repository on a cron schedule, with nobody watching.
|
|
4
|
+
|
|
5
|
+
## Set up
|
|
6
|
+
|
|
7
|
+
1. Copy `prompt.md`. Its only placeholder is `{{name}}`, your project name. The install and test commands come from your `CLAUDE.md`.
|
|
8
|
+
2. In Claude Code on the web, create a routine for the repository and paste the prompt. Give it write access to `main` only.
|
|
9
|
+
3. Pick a schedule (see below). `loop` generates a staggered cron line for you.
|
|
10
|
+
|
|
11
|
+
## Staggered cron
|
|
12
|
+
|
|
13
|
+
If several loops (or several lanes of one project) run against one repository, give each a different minute so they do not collide. Use the pattern `M * * * *` (hourly), and move `M` by at least 10 minutes per loop, for example `7 * * * *`, `17 * * * *`, `27 * * * *`. Avoid minute `0`: everyone's schedule lands there.
|
|
14
|
+
|
|
15
|
+
Each run stops after about 40 minutes of work, so an hourly schedule leaves a gap before the next one. The 90-minute session lock (`.ai/session.lock`) stops two runs from working at once. The run budget (`Run: N / 30` in `.ai/task.md`) caps the total number of runs.
|
|
16
|
+
|
|
17
|
+
## Safety
|
|
18
|
+
|
|
19
|
+
The prompt never publishes, spends money or stores a secret. The routine uses your Claude account; no API key goes into the repository.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You are one scheduled run of the build loop for this repository ({{name}}). Nobody is watching; the owner reads the results later. Read CLAUDE.md and follow it exactly, especially "Unattended mode". IDEA.md is the brief. Your task file is `.ai/task.md`.
|
|
2
|
+
|
|
3
|
+
0. Run `date -u` and note it as this run's start time. Run `git checkout main && git pull --ff-only`. Every commit goes to `main` and is pushed with `git push origin main`. Never create or push a `claude/` branch: the next run starts from `main` and would never see it. If a push is rejected, run `git pull --rebase`, re-run the checks if incoming commits touched code, and push again.
|
|
4
|
+
1. Session lock: if `.ai/session.lock` holds a UTC time less than 90 minutes old, another session owns the repo. Reply "Nothing to do" and stop. If the file is older than that, it is stale: ignore it.
|
|
5
|
+
2. If `DONE.md` or `BLOCKED.md` exists, reply "Nothing to do" and stop.
|
|
6
|
+
3. Run the install command from CLAUDE.md (`npm install` unless it says otherwise). If it fails, write `BLOCKED.md` with the error and stop.
|
|
7
|
+
4. If `.ai/task.md` exists, run the resume protocol (.ai/method.md §9.2) and add 1 to its `Run: N / 30` counter. When the counter reaches 30, write `BLOCKED.md` saying "run budget reached", push, and stop. If there is no task file, write one from IDEA.md first.
|
|
8
|
+
5. Run the test command from CLAUDE.md (`npm test` unless it says otherwise) on the fresh pull. If it fails, fixing it is your first unit ("main stays green"). Otherwise take the next unit from the task file. Do one unit at a time.
|
|
9
|
+
6. After each unit run the test command from CLAUDE.md (`npm test` unless it says otherwise). Green: commit, have a reviewer subagent that did not write the code check the diff against IDEA.md ("find the three most likely ways this is wrong for a real user, and check each"), fix any blockers, run the tests again, pull, then push to `main`. Red: write the attempt and the reason under Tried in the task file, then discard the unit's uncommitted changes. The same failure twice means the stuck protocol (§9.3); its last step is `BLOCKED.md`.
|
|
10
|
+
7. A pushed unit does not end the run. Keep taking units until one of these is true: 40 minutes have passed since the start time (check `date -u` after each push), your context is about 60% full, or no unit is left.
|
|
11
|
+
8. Run the handoff (§9.1) in the task file so a fresh run with zero memory can continue at full quality. Commit it, push to `main`, and stop. If every done-when in the contract passes and the reviewer finds no blockers, run the retro (§12), write `DONE.md` (what was built, test results, anything unsure, the 10 best next improvements ranked by value) and push instead.
|
|
12
|
+
|
|
13
|
+
Never spend money, sign up for anything, create accounts, publish anything, add API keys, secrets, ads, analytics or tracking, copy other projects' content without a compatible licence, force-push, rewrite history, or touch anything outside this repository.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Project core
|
|
4
|
+
|
|
5
|
+
**What this is:** a Chrome extension (Manifest V3) built by an unattended AI loop. The goal and must-haves are in [IDEA.md](IDEA.md).
|
|
6
|
+
|
|
7
|
+
**Commands**
|
|
8
|
+
- Install: `npm install`, then `npx playwright install chromium`. If the download is blocked, `playwright.config.js` finds a preinstalled Chromium or system Chrome.
|
|
9
|
+
- Test (the only judge of "done"): `npm test` = `check` + the Playwright extension test.
|
|
10
|
+
- Fast: `npm run check` (manifest, permissions, MV3 rules; no browser).
|
|
11
|
+
|
|
12
|
+
**Hard rules**
|
|
13
|
+
- All extension files live in `extension/`. No inline scripts or handlers (MV3 blocks them) and no remotely hosted code.
|
|
14
|
+
- Narrowest permissions only. Anything beyond those listed in IDEA.md needs the owner.
|
|
15
|
+
- Service workers stop when idle: keep state in `chrome.storage`, never in module variables.
|
|
16
|
+
- Every must-have in IDEA.md gets a Playwright test in `tests/` that loads the unpacked extension.
|
|
17
|
+
- No telemetry, analytics, ads or calls home. No secrets in the repository.
|
|
18
|
+
- Never publish to the Chrome Web Store, spend money or create accounts.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# IDEA
|
|
2
|
+
|
|
3
|
+
> The owner fills this in. It's the only input the loop gets, so the more specific, the better.
|
|
4
|
+
|
|
5
|
+
## The extension in one sentence
|
|
6
|
+
<e.g. "A popup that shows how many tabs are open and closes duplicates.">
|
|
7
|
+
|
|
8
|
+
## Who it's for
|
|
9
|
+
<e.g. "People who keep dozens of tabs open while researching.">
|
|
10
|
+
|
|
11
|
+
## Must have (v1)
|
|
12
|
+
- <the main feature, observable in the popup or on a page>
|
|
13
|
+
- <...>
|
|
14
|
+
|
|
15
|
+
## Permissions allowed
|
|
16
|
+
<List each permission the extension may ask for, and why. Anything not listed needs the owner.>
|
|
17
|
+
|
|
18
|
+
## Nice to have (only after v1 works)
|
|
19
|
+
- <...>
|
|
20
|
+
|
|
21
|
+
## Run budget
|
|
22
|
+
30
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Service workers stop after ~30 s idle, so keep state in chrome.storage, never in module variables.
|
|
2
|
+
chrome.runtime.onInstalled.addListener(async () => {
|
|
3
|
+
const { clicks } = await chrome.storage.local.get('clicks');
|
|
4
|
+
if (clicks === undefined) await chrome.storage.local.set({ clicks: 0 });
|
|
5
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "{{name}}",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "A Chrome extension built and verified by an unattended Compounding Loop.",
|
|
6
|
+
"action": { "default_popup": "popup.html", "default_title": "{{name}}" },
|
|
7
|
+
"background": { "service_worker": "background.js" },
|
|
8
|
+
"permissions": ["storage"]
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{{name}}</title>
|
|
7
|
+
<link rel="stylesheet" href="popup.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<h1>{{name}}</h1>
|
|
11
|
+
<p>Clicks so far: <strong id="count">0</strong></p>
|
|
12
|
+
<button id="click" type="button">Click me</button>
|
|
13
|
+
<script type="module" src="popup.js"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// MV3 forbids inline scripts, so the popup logic lives in this file.
|
|
2
|
+
const countEl = document.getElementById('count');
|
|
3
|
+
const button = document.getElementById('click');
|
|
4
|
+
|
|
5
|
+
async function render() {
|
|
6
|
+
const { clicks = 0 } = await chrome.storage.local.get('clicks');
|
|
7
|
+
countEl.textContent = String(clicks);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
button.addEventListener('click', async () => {
|
|
11
|
+
const { clicks = 0 } = await chrome.storage.local.get('clicks');
|
|
12
|
+
await chrome.storage.local.set({ clicks: clicks + 1 });
|
|
13
|
+
await render();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
render();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig, chromium } from '@playwright/test';
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// Cloud sandboxes can block Playwright's own browser download. If that browser is missing, use a preinstalled
|
|
5
|
+
// Playwright Chromium (Claude's cloud VMs have one in /opt/pw-browsers) or a system Chrome.
|
|
6
|
+
const bundled = (() => { try { return chromium.executablePath(); } catch { return ''; } })();
|
|
7
|
+
const preinstalled = ['/opt/pw-browsers', `${process.env.HOME}/.cache/ms-playwright`].flatMap((dir) => {
|
|
8
|
+
try {
|
|
9
|
+
return readdirSync(dir).filter((d) => /^chromium-\d+$/.test(d)).sort((a, b) => b.localeCompare(a, 'en', { numeric: true }))
|
|
10
|
+
.flatMap((d) => [`${dir}/${d}/chrome-linux64/chrome`, `${dir}/${d}/chrome-linux/chrome`]);
|
|
11
|
+
} catch { return []; }
|
|
12
|
+
});
|
|
13
|
+
const systemChrome = [process.env.CHROME_PATH, ...preinstalled, '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/opt/google/chrome/chrome']
|
|
14
|
+
.find((p) => p && existsSync(p));
|
|
15
|
+
// Read by tests/popup.spec.js, which launches its own persistent context (extensions need one).
|
|
16
|
+
process.env.CL_CHROME = bundled && existsSync(bundled) ? bundled : systemChrome ?? '';
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
testDir: 'tests',
|
|
20
|
+
timeout: 30_000,
|
|
21
|
+
workers: 1,
|
|
22
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Fast structural checks for extension/. No dependencies, no browser. Messages say how to fix each problem.
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const DIR = resolve(process.argv[2] ?? 'extension');
|
|
6
|
+
const errors = [];
|
|
7
|
+
const fail = (file, msg) => errors.push(`${file}: ${msg}`);
|
|
8
|
+
|
|
9
|
+
const manifestPath = join(DIR, 'manifest.json');
|
|
10
|
+
let manifest = null;
|
|
11
|
+
try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); } catch { fail('manifest.json', 'missing or not valid JSON.'); }
|
|
12
|
+
|
|
13
|
+
if (manifest) {
|
|
14
|
+
if (manifest.manifest_version !== 3) fail('manifest.json', '"manifest_version" must be 3; Chrome no longer accepts version 2.');
|
|
15
|
+
for (const key of ['name', 'version', 'description']) if (!manifest[key]) fail('manifest.json', `add "${key}".`);
|
|
16
|
+
if (manifest.version && !/^\d+(\.\d+){0,3}$/.test(manifest.version)) fail('manifest.json', `"version" ${manifest.version} must be 1-4 dot-separated integers.`);
|
|
17
|
+
if (manifest.description && manifest.description.length > 132) fail('manifest.json', 'description is over 132 characters; the Chrome Web Store rejects it.');
|
|
18
|
+
|
|
19
|
+
const files = [manifest.action?.default_popup, manifest.background?.service_worker, ...Object.values(manifest.icons ?? {})].filter(Boolean);
|
|
20
|
+
for (const f of files) if (!existsSync(join(DIR, f))) fail('manifest.json', `references "${f}", which does not exist in extension/.`);
|
|
21
|
+
|
|
22
|
+
// Least privilege: broad host access and risky permissions need a reason from the owner.
|
|
23
|
+
const risky = [...(manifest.host_permissions ?? []), ...(manifest.permissions ?? [])].filter((p) => /<all_urls>|\*:\/\/\*\/\*|^(tabs|history|cookies|webRequest|debugger)$/.test(p));
|
|
24
|
+
if (risky.length) fail('manifest.json', `broad permissions (${risky.join(', ')}). Use the narrowest permission that works, or ask the owner.`);
|
|
25
|
+
if (manifest.content_security_policy?.extension_pages && /unsafe-(inline|eval)/.test(manifest.content_security_policy.extension_pages)) fail('manifest.json', 'CSP allows unsafe-inline or unsafe-eval.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// MV3 blocks inline scripts and remote code.
|
|
29
|
+
const popup = manifest?.action?.default_popup;
|
|
30
|
+
if (popup && existsSync(join(DIR, popup))) {
|
|
31
|
+
const html = readFileSync(join(DIR, popup), 'utf8');
|
|
32
|
+
for (const [tag] of html.matchAll(/<script\b[^>]*>/gi)) {
|
|
33
|
+
const src = tag.match(/\ssrc="([^"]+)"/i)?.[1];
|
|
34
|
+
if (!src) fail(popup, 'inline <script>. Move it to a .js file; MV3 blocks inline scripts.');
|
|
35
|
+
else if (/^(https?:)?\/\//i.test(src)) fail(popup, `remote script "${src}". MV3 forbids remotely hosted code; bundle it in extension/.`);
|
|
36
|
+
}
|
|
37
|
+
if (/\son[a-z]+\s*=\s*["']/i.test(html)) fail(popup, 'inline event handler. Use addEventListener in a .js file.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Secrets must never be committed.
|
|
41
|
+
const SECRET = /(\bsk-(ant-)?[A-Za-z0-9_-]{20,}|\bAKIA[0-9A-Z]{16}|\bgh[pousr]_[A-Za-z0-9]{30,}|-----BEGIN [A-Z ]*PRIVATE KEY-----)/;
|
|
42
|
+
for (const f of ['background.js', 'popup.js', 'popup.html', 'manifest.json']) {
|
|
43
|
+
const p = join(DIR, f);
|
|
44
|
+
if (existsSync(p) && SECRET.test(readFileSync(p, 'utf8'))) fail(f, 'looks like it contains an API key or private key. Remove it.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (errors.length) {
|
|
48
|
+
console.error(`check: ${errors.length} problem(s)\n- ${errors.join('\n- ')}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
console.log('check: OK');
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// `npm test`: runs each test step in order (stopping at the first failure, like `&&`) and writes the
|
|
2
|
+
// totals to .ai/last-test.json, which the Compounding Loop dashboard's "failing tests" health check reads.
|
|
3
|
+
// Commit that file with your work so the dashboard sees it whichever machine ran the tests.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
/** Totals from test-runner output: node:test (`ℹ pass 3` or TAP `# pass 3`) and Playwright (`12 passed`, `1 failed`). */
|
|
10
|
+
export function countResults(output) {
|
|
11
|
+
const plain = output.replace(/\x1b\[[0-9;]*m/g, '');
|
|
12
|
+
const sum = (re) => [...plain.matchAll(re)].reduce((n, m) => n + Number(m[1]), 0);
|
|
13
|
+
const passed = sum(/^\s*(?:ℹ|#) pass (\d+)\s*$/gm) + sum(/^\s*(\d+) passed\b/gm);
|
|
14
|
+
const failed = sum(/^\s*(?:ℹ|#) fail (\d+)\s*$/gm) + sum(/^\s*(\d+) failed\b/gm);
|
|
15
|
+
return passed + failed > 0 ? { passed, failed } : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Steps this repo has, as [label, node arguments]. Node runs them directly, so no shell or .cmd shim is involved. */
|
|
19
|
+
function steps() {
|
|
20
|
+
const list = [['check', ['scripts/check.mjs']]];
|
|
21
|
+
if (existsSync('tests/unit')) list.push(['unit', ['--test', 'tests/unit/**/*.test.mjs']]);
|
|
22
|
+
if (existsSync('playwright.config.js')) list.push(['browser', [path.join('node_modules', '@playwright', 'test', 'cli.js'), 'test', ...process.argv.slice(2)]]);
|
|
23
|
+
return list;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function run(args) {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
let output = '';
|
|
29
|
+
const child = spawn(process.execPath, args, { stdio: ['inherit', 'pipe', 'pipe'] });
|
|
30
|
+
child.stdout.on('data', (chunk) => { output += chunk; process.stdout.write(chunk); });
|
|
31
|
+
child.stderr.on('data', (chunk) => { output += chunk; process.stderr.write(chunk); });
|
|
32
|
+
child.on('error', (error) => { output += String(error); resolve({ code: 1, output }); });
|
|
33
|
+
child.on('close', (code) => resolve({ code: code ?? 1, output }));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const total = { passed: 0, failed: 0 };
|
|
39
|
+
let code = 0;
|
|
40
|
+
for (const [label, args] of steps()) {
|
|
41
|
+
const result = await run(args);
|
|
42
|
+
// A step without countable output (the check script) counts as one test.
|
|
43
|
+
const counts = countResults(result.output) ?? (result.code === 0 ? { passed: 1, failed: 0 } : { passed: 0, failed: 1 });
|
|
44
|
+
if (result.code !== 0 && counts.failed === 0) counts.failed = 1;
|
|
45
|
+
total.passed += counts.passed;
|
|
46
|
+
total.failed += counts.failed;
|
|
47
|
+
if (result.code !== 0) {
|
|
48
|
+
console.error(`\n${label} failed (exit ${result.code}); later steps skipped.`);
|
|
49
|
+
code = result.code;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
mkdirSync('.ai', { recursive: true });
|
|
54
|
+
writeFileSync(path.join('.ai', 'last-test.json'), `${JSON.stringify({ ...total, at: new Date().toISOString() })}\n`);
|
|
55
|
+
console.log(`\n${total.passed} passed, ${total.failed} failed (written to .ai/last-test.json)`);
|
|
56
|
+
process.exitCode = code;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Loads the unpacked extension in a real Chromium and drives its popup.
|
|
2
|
+
import { test, expect, chromium } from '@playwright/test';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const extension = resolve('extension');
|
|
8
|
+
|
|
9
|
+
test('popup counts clicks and keeps them in chrome.storage', async () => {
|
|
10
|
+
const userDataDir = mkdtempSync(join(tmpdir(), 'ext-'));
|
|
11
|
+
const context = await chromium.launchPersistentContext(userDataDir, {
|
|
12
|
+
...(process.env.CL_CHROME ? { executablePath: process.env.CL_CHROME } : {}),
|
|
13
|
+
args: [`--disable-extensions-except=${extension}`, `--load-extension=${extension}`, '--headless=new'],
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
const worker = context.serviceWorkers()[0] ?? (await context.waitForEvent('serviceworker'));
|
|
17
|
+
const id = new URL(worker.url()).host;
|
|
18
|
+
|
|
19
|
+
const problems = [];
|
|
20
|
+
const popup = await context.newPage();
|
|
21
|
+
popup.on('pageerror', (e) => problems.push(`JS error: ${e.message}`));
|
|
22
|
+
popup.on('console', (m) => m.type() === 'error' && problems.push(`console error: ${m.text()}`));
|
|
23
|
+
await popup.goto(`chrome-extension://${id}/popup.html`);
|
|
24
|
+
|
|
25
|
+
await expect(popup.locator('h1')).toBeVisible();
|
|
26
|
+
await expect(popup.locator('#count')).toHaveText('0');
|
|
27
|
+
await popup.locator('#click').click();
|
|
28
|
+
await expect(popup.locator('#count')).toHaveText('1');
|
|
29
|
+
|
|
30
|
+
await popup.reload();
|
|
31
|
+
await expect(popup.locator('#count'), 'the count must survive a reload').toHaveText('1');
|
|
32
|
+
expect(problems, 'fix these errors').toEqual([]);
|
|
33
|
+
} finally {
|
|
34
|
+
await context.close();
|
|
35
|
+
rmSync(userDataDir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|