darkfire514 commited on
Commit
d578cc9
·
verified ·
1 Parent(s): fb4d8fe

Upload 23 files

Browse files
.github/FUNDING.yml ADDED
@@ -0,0 +1 @@
 
 
1
+ custom: ["https://github.com/sponsors/steipete"]
.github/ISSUE_TEMPLATE/bug_report.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Bug report
3
+ about: Report a problem or unexpected behavior in Clawdbot.
4
+ title: "[Bug]: "
5
+ labels: bug
6
+ ---
7
+
8
+ ## Summary
9
+
10
+ What went wrong?
11
+
12
+ ## Steps to reproduce
13
+
14
+ 1.
15
+ 2.
16
+ 3.
17
+
18
+ ## Expected behavior
19
+
20
+ What did you expect to happen?
21
+
22
+ ## Actual behavior
23
+
24
+ What actually happened?
25
+
26
+ ## Environment
27
+
28
+ - Clawdbot version:
29
+ - OS:
30
+ - Install method (pnpm/npx/docker/etc):
31
+
32
+ ## Logs or screenshots
33
+
34
+ Paste relevant logs or add screenshots (redact secrets).
.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Onboarding
4
+ url: https://discord.gg/clawd
5
+ about: New to Clawdbot? Join Discord for setup guidance from Krill in #help.
6
+ - name: Support
7
+ url: https://discord.gg/clawd
8
+ about: Get help from Krill and the community on Discord in #help.
.github/ISSUE_TEMPLATE/feature_request.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea or improvement for Clawdbot.
4
+ title: "[Feature]: "
5
+ labels: enhancement
6
+ ---
7
+
8
+ ## Summary
9
+
10
+ Describe the problem you are trying to solve or the opportunity you see.
11
+
12
+ ## Proposed solution
13
+
14
+ What would you like Clawdbot to do?
15
+
16
+ ## Alternatives considered
17
+
18
+ Any other approaches you have considered?
19
+
20
+ ## Additional context
21
+
22
+ Links, screenshots, or related issues.
.github/actionlint.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # actionlint configuration
2
+ # https://github.com/rhysd/actionlint/blob/main/docs/config.md
3
+
4
+ self-hosted-runner:
5
+ labels:
6
+ # Blacksmith CI runners
7
+ - blacksmith-4vcpu-ubuntu-2404
8
+ - blacksmith-4vcpu-windows-2025
9
+
10
+ # Ignore patterns for known issues
11
+ paths:
12
+ .github/workflows/**/*.yml:
13
+ ignore:
14
+ # Ignore shellcheck warnings (we run shellcheck separately)
15
+ - "shellcheck reported issue.+"
16
+ # Ignore intentional if: false for disabled jobs
17
+ - 'constant expression "false" in condition'
.github/dependabot.yml ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependabot configuration
2
+ # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
3
+
4
+ version: 2
5
+
6
+ registries:
7
+ npm-npmjs:
8
+ type: npm-registry
9
+ url: https://registry.npmjs.org
10
+ replaces-base: true
11
+
12
+ updates:
13
+ # npm dependencies (root)
14
+ - package-ecosystem: npm
15
+ directory: /
16
+ schedule:
17
+ interval: weekly
18
+ cooldown:
19
+ default-days: 7
20
+ groups:
21
+ production:
22
+ dependency-type: production
23
+ update-types:
24
+ - minor
25
+ - patch
26
+ development:
27
+ dependency-type: development
28
+ update-types:
29
+ - minor
30
+ - patch
31
+ open-pull-requests-limit: 10
32
+ registries:
33
+ - npm-npmjs
34
+
35
+ # GitHub Actions
36
+ - package-ecosystem: github-actions
37
+ directory: /
38
+ schedule:
39
+ interval: weekly
40
+ cooldown:
41
+ default-days: 7
42
+ groups:
43
+ actions:
44
+ patterns:
45
+ - "*"
46
+ update-types:
47
+ - minor
48
+ - patch
49
+ open-pull-requests-limit: 5
50
+
51
+ # Swift Package Manager - macOS app
52
+ - package-ecosystem: swift
53
+ directory: /apps/macos
54
+ schedule:
55
+ interval: weekly
56
+ cooldown:
57
+ default-days: 7
58
+ groups:
59
+ swift-deps:
60
+ patterns:
61
+ - "*"
62
+ update-types:
63
+ - minor
64
+ - patch
65
+ open-pull-requests-limit: 5
66
+
67
+ # Swift Package Manager - shared MoltbotKit
68
+ - package-ecosystem: swift
69
+ directory: /apps/shared/MoltbotKit
70
+ schedule:
71
+ interval: weekly
72
+ cooldown:
73
+ default-days: 7
74
+ groups:
75
+ swift-deps:
76
+ patterns:
77
+ - "*"
78
+ update-types:
79
+ - minor
80
+ - patch
81
+ open-pull-requests-limit: 5
82
+
83
+ # Swift Package Manager - Swabble
84
+ - package-ecosystem: swift
85
+ directory: /Swabble
86
+ schedule:
87
+ interval: weekly
88
+ cooldown:
89
+ default-days: 7
90
+ groups:
91
+ swift-deps:
92
+ patterns:
93
+ - "*"
94
+ update-types:
95
+ - minor
96
+ - patch
97
+ open-pull-requests-limit: 5
98
+
99
+ # Gradle - Android app
100
+ - package-ecosystem: gradle
101
+ directory: /apps/android
102
+ schedule:
103
+ interval: weekly
104
+ cooldown:
105
+ default-days: 7
106
+ groups:
107
+ android-deps:
108
+ patterns:
109
+ - "*"
110
+ update-types:
111
+ - minor
112
+ - patch
113
+ open-pull-requests-limit: 5
.github/labeler.yml ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "channel: bluebubbles":
2
+ - changed-files:
3
+ - any-glob-to-any-file:
4
+ - "extensions/bluebubbles/**"
5
+ - "docs/channels/bluebubbles.md"
6
+ "channel: discord":
7
+ - changed-files:
8
+ - any-glob-to-any-file:
9
+ - "src/discord/**"
10
+ - "extensions/discord/**"
11
+ - "docs/channels/discord.md"
12
+ "channel: googlechat":
13
+ - changed-files:
14
+ - any-glob-to-any-file:
15
+ - "extensions/googlechat/**"
16
+ - "docs/channels/googlechat.md"
17
+ "channel: imessage":
18
+ - changed-files:
19
+ - any-glob-to-any-file:
20
+ - "src/imessage/**"
21
+ - "extensions/imessage/**"
22
+ - "docs/channels/imessage.md"
23
+ "channel: line":
24
+ - changed-files:
25
+ - any-glob-to-any-file:
26
+ - "extensions/line/**"
27
+ - "docs/channels/line.md"
28
+ "channel: matrix":
29
+ - changed-files:
30
+ - any-glob-to-any-file:
31
+ - "extensions/matrix/**"
32
+ - "docs/channels/matrix.md"
33
+ "channel: mattermost":
34
+ - changed-files:
35
+ - any-glob-to-any-file:
36
+ - "extensions/mattermost/**"
37
+ - "docs/channels/mattermost.md"
38
+ "channel: msteams":
39
+ - changed-files:
40
+ - any-glob-to-any-file:
41
+ - "extensions/msteams/**"
42
+ - "docs/channels/msteams.md"
43
+ "channel: nextcloud-talk":
44
+ - changed-files:
45
+ - any-glob-to-any-file:
46
+ - "extensions/nextcloud-talk/**"
47
+ - "docs/channels/nextcloud-talk.md"
48
+ "channel: nostr":
49
+ - changed-files:
50
+ - any-glob-to-any-file:
51
+ - "extensions/nostr/**"
52
+ - "docs/channels/nostr.md"
53
+ "channel: signal":
54
+ - changed-files:
55
+ - any-glob-to-any-file:
56
+ - "src/signal/**"
57
+ - "extensions/signal/**"
58
+ - "docs/channels/signal.md"
59
+ "channel: slack":
60
+ - changed-files:
61
+ - any-glob-to-any-file:
62
+ - "src/slack/**"
63
+ - "extensions/slack/**"
64
+ - "docs/channels/slack.md"
65
+ "channel: telegram":
66
+ - changed-files:
67
+ - any-glob-to-any-file:
68
+ - "src/telegram/**"
69
+ - "extensions/telegram/**"
70
+ - "docs/channels/telegram.md"
71
+ "channel: tlon":
72
+ - changed-files:
73
+ - any-glob-to-any-file:
74
+ - "extensions/tlon/**"
75
+ - "docs/channels/tlon.md"
76
+ "channel: voice-call":
77
+ - changed-files:
78
+ - any-glob-to-any-file:
79
+ - "extensions/voice-call/**"
80
+ "channel: whatsapp-web":
81
+ - changed-files:
82
+ - any-glob-to-any-file:
83
+ - "src/web/**"
84
+ - "extensions/whatsapp/**"
85
+ - "docs/channels/whatsapp.md"
86
+ "channel: zalo":
87
+ - changed-files:
88
+ - any-glob-to-any-file:
89
+ - "extensions/zalo/**"
90
+ - "docs/channels/zalo.md"
91
+ "channel: zalouser":
92
+ - changed-files:
93
+ - any-glob-to-any-file:
94
+ - "extensions/zalouser/**"
95
+ - "docs/channels/zalouser.md"
96
+
97
+ "app: android":
98
+ - changed-files:
99
+ - any-glob-to-any-file:
100
+ - "apps/android/**"
101
+ - "docs/platforms/android.md"
102
+ "app: ios":
103
+ - changed-files:
104
+ - any-glob-to-any-file:
105
+ - "apps/ios/**"
106
+ - "docs/platforms/ios.md"
107
+ "app: macos":
108
+ - changed-files:
109
+ - any-glob-to-any-file:
110
+ - "apps/macos/**"
111
+ - "docs/platforms/macos.md"
112
+ - "docs/platforms/mac/**"
113
+ "app: web-ui":
114
+ - changed-files:
115
+ - any-glob-to-any-file:
116
+ - "ui/**"
117
+ - "src/gateway/control-ui.ts"
118
+ - "src/gateway/control-ui-shared.ts"
119
+ - "src/gateway/protocol/**"
120
+ - "src/gateway/server-methods/chat.ts"
121
+ - "src/infra/control-ui-assets.ts"
122
+
123
+ "gateway":
124
+ - changed-files:
125
+ - any-glob-to-any-file:
126
+ - "src/gateway/**"
127
+ - "src/daemon/**"
128
+ - "docs/gateway/**"
129
+
130
+ "docs":
131
+ - changed-files:
132
+ - any-glob-to-any-file:
133
+ - "docs/**"
134
+ - "docs.acp.md"
135
+
136
+ "cli":
137
+ - changed-files:
138
+ - any-glob-to-any-file:
139
+ - "src/cli/**"
140
+
141
+ "commands":
142
+ - changed-files:
143
+ - any-glob-to-any-file:
144
+ - "src/commands/**"
145
+
146
+ "scripts":
147
+ - changed-files:
148
+ - any-glob-to-any-file:
149
+ - "scripts/**"
150
+
151
+ "docker":
152
+ - changed-files:
153
+ - any-glob-to-any-file:
154
+ - "Dockerfile"
155
+ - "Dockerfile.*"
156
+ - "docker-compose.yml"
157
+ - "docker-setup.sh"
158
+ - ".dockerignore"
159
+ - "scripts/**/*docker*"
160
+ - "scripts/**/Dockerfile*"
161
+ - "scripts/sandbox-*.sh"
162
+ - "src/agents/sandbox*.ts"
163
+ - "src/commands/sandbox*.ts"
164
+ - "src/cli/sandbox-cli.ts"
165
+ - "src/docker-setup.test.ts"
166
+ - "src/config/**/*sandbox*"
167
+ - "docs/cli/sandbox.md"
168
+ - "docs/gateway/sandbox*.md"
169
+ - "docs/install/docker.md"
170
+ - "docs/multi-agent-sandbox-tools.md"
171
+
172
+ "agents":
173
+ - changed-files:
174
+ - any-glob-to-any-file:
175
+ - "src/agents/**"
176
+
177
+ "security":
178
+ - changed-files:
179
+ - any-glob-to-any-file:
180
+ - "docs/cli/security.md"
181
+ - "docs/gateway/security.md"
182
+
183
+ "extensions: copilot-proxy":
184
+ - changed-files:
185
+ - any-glob-to-any-file:
186
+ - "extensions/copilot-proxy/**"
187
+ "extensions: diagnostics-otel":
188
+ - changed-files:
189
+ - any-glob-to-any-file:
190
+ - "extensions/diagnostics-otel/**"
191
+ "extensions: google-antigravity-auth":
192
+ - changed-files:
193
+ - any-glob-to-any-file:
194
+ - "extensions/google-antigravity-auth/**"
195
+ "extensions: google-gemini-cli-auth":
196
+ - changed-files:
197
+ - any-glob-to-any-file:
198
+ - "extensions/google-gemini-cli-auth/**"
199
+ "extensions: llm-task":
200
+ - changed-files:
201
+ - any-glob-to-any-file:
202
+ - "extensions/llm-task/**"
203
+ "extensions: lobster":
204
+ - changed-files:
205
+ - any-glob-to-any-file:
206
+ - "extensions/lobster/**"
207
+ "extensions: memory-core":
208
+ - changed-files:
209
+ - any-glob-to-any-file:
210
+ - "extensions/memory-core/**"
211
+ "extensions: memory-lancedb":
212
+ - changed-files:
213
+ - any-glob-to-any-file:
214
+ - "extensions/memory-lancedb/**"
215
+ "extensions: open-prose":
216
+ - changed-files:
217
+ - any-glob-to-any-file:
218
+ - "extensions/open-prose/**"
219
+ "extensions: qwen-portal-auth":
220
+ - changed-files:
221
+ - any-glob-to-any-file:
222
+ - "extensions/qwen-portal-auth/**"
.github/workflows/auto-response.yml ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Auto response
2
+
3
+ on:
4
+ issues:
5
+ types: [opened, edited, labeled]
6
+ pull_request_target:
7
+ types: [labeled]
8
+
9
+ permissions: {}
10
+
11
+ jobs:
12
+ auto-response:
13
+ permissions:
14
+ issues: write
15
+ pull-requests: write
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
19
+ id: app-token
20
+ with:
21
+ app-id: "2729701"
22
+ private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
23
+ - name: Handle labeled items
24
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
25
+ with:
26
+ github-token: ${{ steps.app-token.outputs.token }}
27
+ script: |
28
+ // Labels prefixed with "r:" are auto-response triggers.
29
+ const rules = [
30
+ {
31
+ label: "r: skill",
32
+ close: true,
33
+ message:
34
+ "Thanks for the contribution! New skills should be published to [Clawhub](https://clawhub.ai) for everyone to use. We’re keeping the core lean on skills, so I’m closing this out.",
35
+ },
36
+ {
37
+ label: "r: support",
38
+ close: true,
39
+ message:
40
+ "Please use [our support server](https://discord.gg/clawd) and ask in #help or #users-helping-users to resolve this, or follow the stuck FAQ at https://docs.openclaw.ai/help/faq#im-stuck-whats-the-fastest-way-to-get-unstuck.",
41
+ },
42
+ {
43
+ label: "r: third-party-extension",
44
+ close: true,
45
+ message:
46
+ "This would be better made as a third-party extension with our SDK that you maintain yourself. Docs: https://docs.openclaw.ai/plugin.",
47
+ },
48
+ {
49
+ label: "r: moltbook",
50
+ close: true,
51
+ lock: true,
52
+ lockReason: "off-topic",
53
+ message:
54
+ "OpenClaw is not affiliated with Moltbook, and issues related to Moltbook should not be submitted here.",
55
+ },
56
+ ];
57
+
58
+ const issue = context.payload.issue;
59
+ if (issue) {
60
+ const title = issue.title ?? "";
61
+ const body = issue.body ?? "";
62
+ const haystack = `${title}\n${body}`.toLowerCase();
63
+ const hasLabel = (issue.labels ?? []).some((label) =>
64
+ typeof label === "string" ? label === "r: moltbook" : label?.name === "r: moltbook",
65
+ );
66
+ if (haystack.includes("moltbook") && !hasLabel) {
67
+ await github.rest.issues.addLabels({
68
+ owner: context.repo.owner,
69
+ repo: context.repo.repo,
70
+ issue_number: issue.number,
71
+ labels: ["r: moltbook"],
72
+ });
73
+ return;
74
+ }
75
+ }
76
+
77
+ const labelName = context.payload.label?.name;
78
+ if (!labelName) {
79
+ return;
80
+ }
81
+
82
+ const rule = rules.find((item) => item.label === labelName);
83
+ if (!rule) {
84
+ return;
85
+ }
86
+
87
+ const issueNumber = context.payload.issue?.number ?? context.payload.pull_request?.number;
88
+ if (!issueNumber) {
89
+ return;
90
+ }
91
+
92
+ await github.rest.issues.createComment({
93
+ owner: context.repo.owner,
94
+ repo: context.repo.repo,
95
+ issue_number: issueNumber,
96
+ body: rule.message,
97
+ });
98
+
99
+ if (rule.close) {
100
+ await github.rest.issues.update({
101
+ owner: context.repo.owner,
102
+ repo: context.repo.repo,
103
+ issue_number: issueNumber,
104
+ state: "closed",
105
+ });
106
+ }
107
+
108
+ if (rule.lock) {
109
+ await github.rest.issues.lock({
110
+ owner: context.repo.owner,
111
+ repo: context.repo.repo,
112
+ issue_number: issueNumber,
113
+ lock_reason: rule.lockReason ?? "resolved",
114
+ });
115
+ }
.github/workflows/ci.yml ADDED
@@ -0,0 +1,644 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ install-check:
9
+ runs-on: blacksmith-4vcpu-ubuntu-2404
10
+ steps:
11
+ - name: Checkout
12
+ uses: actions/checkout@v4
13
+ with:
14
+ submodules: false
15
+
16
+ - name: Checkout submodules (retry)
17
+ run: |
18
+ set -euo pipefail
19
+ git submodule sync --recursive
20
+ for attempt in 1 2 3 4 5; do
21
+ if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
22
+ exit 0
23
+ fi
24
+ echo "Submodule update failed (attempt $attempt/5). Retrying…"
25
+ sleep $((attempt * 10))
26
+ done
27
+ exit 1
28
+
29
+ - name: Setup Node.js
30
+ uses: actions/setup-node@v4
31
+ with:
32
+ node-version: 22.x
33
+ check-latest: true
34
+
35
+ - name: Setup pnpm (corepack retry)
36
+ run: |
37
+ set -euo pipefail
38
+ corepack enable
39
+ for attempt in 1 2 3; do
40
+ if corepack prepare pnpm@10.23.0 --activate; then
41
+ pnpm -v
42
+ exit 0
43
+ fi
44
+ echo "corepack prepare failed (attempt $attempt/3). Retrying..."
45
+ sleep $((attempt * 10))
46
+ done
47
+ exit 1
48
+
49
+ - name: Runtime versions
50
+ run: |
51
+ node -v
52
+ npm -v
53
+ pnpm -v
54
+
55
+ - name: Capture node path
56
+ run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
57
+
58
+ - name: Install dependencies (frozen)
59
+ env:
60
+ CI: true
61
+ run: |
62
+ export PATH="$NODE_BIN:$PATH"
63
+ which node
64
+ node -v
65
+ pnpm -v
66
+ pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
67
+
68
+ checks:
69
+ runs-on: blacksmith-4vcpu-ubuntu-2404
70
+ strategy:
71
+ fail-fast: false
72
+ matrix:
73
+ include:
74
+ - runtime: node
75
+ task: tsgo
76
+ command: pnpm tsgo
77
+ - runtime: node
78
+ task: lint
79
+ command: pnpm build && pnpm lint
80
+ - runtime: node
81
+ task: test
82
+ command: pnpm canvas:a2ui:bundle && pnpm test
83
+ - runtime: node
84
+ task: protocol
85
+ command: pnpm protocol:check
86
+ - runtime: node
87
+ task: format
88
+ command: pnpm format
89
+ - runtime: bun
90
+ task: test
91
+ command: pnpm canvas:a2ui:bundle && bunx vitest run
92
+ - runtime: bun
93
+ task: build
94
+ command: bunx tsc -p tsconfig.json --noEmit false
95
+ steps:
96
+ - name: Checkout
97
+ uses: actions/checkout@v4
98
+ with:
99
+ submodules: false
100
+
101
+ - name: Checkout submodules (retry)
102
+ run: |
103
+ set -euo pipefail
104
+ git submodule sync --recursive
105
+ for attempt in 1 2 3 4 5; do
106
+ if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
107
+ exit 0
108
+ fi
109
+ echo "Submodule update failed (attempt $attempt/5). Retrying…"
110
+ sleep $((attempt * 10))
111
+ done
112
+ exit 1
113
+
114
+ - name: Setup Node.js
115
+ uses: actions/setup-node@v4
116
+ with:
117
+ node-version: 22.x
118
+ check-latest: true
119
+
120
+ - name: Setup pnpm (corepack retry)
121
+ run: |
122
+ set -euo pipefail
123
+ corepack enable
124
+ for attempt in 1 2 3; do
125
+ if corepack prepare pnpm@10.23.0 --activate; then
126
+ pnpm -v
127
+ exit 0
128
+ fi
129
+ echo "corepack prepare failed (attempt $attempt/3). Retrying..."
130
+ sleep $((attempt * 10))
131
+ done
132
+ exit 1
133
+
134
+ - name: Setup Bun
135
+ uses: oven-sh/setup-bun@v2
136
+ with:
137
+ bun-version: latest
138
+
139
+ - name: Runtime versions
140
+ run: |
141
+ node -v
142
+ npm -v
143
+ bun -v
144
+ pnpm -v
145
+
146
+ - name: Capture node path
147
+ run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
148
+
149
+ - name: Install dependencies
150
+ env:
151
+ CI: true
152
+ run: |
153
+ export PATH="$NODE_BIN:$PATH"
154
+ which node
155
+ node -v
156
+ pnpm -v
157
+ pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
158
+
159
+ - name: Run ${{ matrix.task }} (${{ matrix.runtime }})
160
+ run: ${{ matrix.command }}
161
+
162
+ secrets:
163
+ runs-on: blacksmith-4vcpu-ubuntu-2404
164
+ steps:
165
+ - name: Checkout
166
+ uses: actions/checkout@v4
167
+ with:
168
+ submodules: false
169
+
170
+ - name: Setup Python
171
+ uses: actions/setup-python@v5
172
+ with:
173
+ python-version: "3.12"
174
+
175
+ - name: Install detect-secrets
176
+ run: |
177
+ python -m pip install --upgrade pip
178
+ python -m pip install detect-secrets==1.5.0
179
+
180
+ - name: Detect secrets
181
+ run: |
182
+ if ! detect-secrets scan --baseline .secrets.baseline; then
183
+ echo "::error::Secret scanning failed. See docs/gateway/security.md#secret-scanning-detect-secrets"
184
+ exit 1
185
+ fi
186
+
187
+ checks-windows:
188
+ runs-on: blacksmith-4vcpu-windows-2025
189
+ env:
190
+ NODE_OPTIONS: --max-old-space-size=4096
191
+ CLAWDBOT_TEST_WORKERS: 1
192
+ defaults:
193
+ run:
194
+ shell: bash
195
+ strategy:
196
+ fail-fast: false
197
+ matrix:
198
+ include:
199
+ - runtime: node
200
+ task: build & lint
201
+ command: pnpm build && pnpm lint
202
+ - runtime: node
203
+ task: test
204
+ command: pnpm canvas:a2ui:bundle && pnpm test
205
+ - runtime: node
206
+ task: protocol
207
+ command: pnpm protocol:check
208
+ steps:
209
+ - name: Checkout
210
+ uses: actions/checkout@v4
211
+ with:
212
+ submodules: false
213
+
214
+ - name: Checkout submodules (retry)
215
+ run: |
216
+ set -euo pipefail
217
+ git submodule sync --recursive
218
+ for attempt in 1 2 3 4 5; do
219
+ if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
220
+ exit 0
221
+ fi
222
+ echo "Submodule update failed (attempt $attempt/5). Retrying…"
223
+ sleep $((attempt * 10))
224
+ done
225
+ exit 1
226
+
227
+ - name: Setup Node.js
228
+ uses: actions/setup-node@v4
229
+ with:
230
+ node-version: 22.x
231
+ check-latest: true
232
+
233
+ - name: Setup pnpm (corepack retry)
234
+ run: |
235
+ set -euo pipefail
236
+ corepack enable
237
+ for attempt in 1 2 3; do
238
+ if corepack prepare pnpm@10.23.0 --activate; then
239
+ pnpm -v
240
+ exit 0
241
+ fi
242
+ echo "corepack prepare failed (attempt $attempt/3). Retrying..."
243
+ sleep $((attempt * 10))
244
+ done
245
+ exit 1
246
+
247
+ - name: Setup Bun
248
+ uses: oven-sh/setup-bun@v2
249
+ with:
250
+ bun-version: latest
251
+
252
+ - name: Runtime versions
253
+ run: |
254
+ node -v
255
+ npm -v
256
+ bun -v
257
+ pnpm -v
258
+
259
+ - name: Capture node path
260
+ run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
261
+
262
+ - name: Install dependencies
263
+ env:
264
+ CI: true
265
+ run: |
266
+ export PATH="$NODE_BIN:$PATH"
267
+ which node
268
+ node -v
269
+ pnpm -v
270
+ pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
271
+
272
+ - name: Run ${{ matrix.task }} (${{ matrix.runtime }})
273
+ run: ${{ matrix.command }}
274
+
275
+ checks-macos:
276
+ if: github.event_name == 'pull_request'
277
+ runs-on: macos-latest
278
+ strategy:
279
+ fail-fast: false
280
+ matrix:
281
+ include:
282
+ - task: test
283
+ command: pnpm test
284
+ steps:
285
+ - name: Checkout
286
+ uses: actions/checkout@v4
287
+ with:
288
+ submodules: false
289
+
290
+ - name: Checkout submodules (retry)
291
+ run: |
292
+ set -euo pipefail
293
+ git submodule sync --recursive
294
+ for attempt in 1 2 3 4 5; do
295
+ if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
296
+ exit 0
297
+ fi
298
+ echo "Submodule update failed (attempt $attempt/5). Retrying…"
299
+ sleep $((attempt * 10))
300
+ done
301
+ exit 1
302
+
303
+ - name: Setup Node.js
304
+ uses: actions/setup-node@v4
305
+ with:
306
+ node-version: 22.x
307
+ check-latest: true
308
+
309
+ - name: Setup pnpm (corepack retry)
310
+ run: |
311
+ set -euo pipefail
312
+ corepack enable
313
+ for attempt in 1 2 3; do
314
+ if corepack prepare pnpm@10.23.0 --activate; then
315
+ pnpm -v
316
+ exit 0
317
+ fi
318
+ echo "corepack prepare failed (attempt $attempt/3). Retrying..."
319
+ sleep $((attempt * 10))
320
+ done
321
+ exit 1
322
+
323
+ - name: Runtime versions
324
+ run: |
325
+ node -v
326
+ npm -v
327
+ pnpm -v
328
+
329
+ - name: Capture node path
330
+ run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
331
+
332
+ - name: Install dependencies
333
+ env:
334
+ CI: true
335
+ run: |
336
+ export PATH="$NODE_BIN:$PATH"
337
+ which node
338
+ node -v
339
+ pnpm -v
340
+ pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
341
+
342
+ - name: Run ${{ matrix.task }}
343
+ env:
344
+ NODE_OPTIONS: --max-old-space-size=4096
345
+ run: ${{ matrix.command }}
346
+
347
+ macos-app:
348
+ if: github.event_name == 'pull_request'
349
+ runs-on: macos-latest
350
+ strategy:
351
+ fail-fast: false
352
+ matrix:
353
+ include:
354
+ - task: lint
355
+ command: |
356
+ swiftlint --config .swiftlint.yml
357
+ swiftformat --lint apps/macos/Sources --config .swiftformat
358
+ - task: build
359
+ command: |
360
+ set -euo pipefail
361
+ for attempt in 1 2 3; do
362
+ if swift build --package-path apps/macos --configuration release; then
363
+ exit 0
364
+ fi
365
+ echo "swift build failed (attempt $attempt/3). Retrying…"
366
+ sleep $((attempt * 20))
367
+ done
368
+ exit 1
369
+ - task: test
370
+ command: |
371
+ set -euo pipefail
372
+ for attempt in 1 2 3; do
373
+ if swift test --package-path apps/macos --parallel --enable-code-coverage --show-codecov-path; then
374
+ exit 0
375
+ fi
376
+ echo "swift test failed (attempt $attempt/3). Retrying…"
377
+ sleep $((attempt * 20))
378
+ done
379
+ exit 1
380
+ steps:
381
+ - name: Checkout
382
+ uses: actions/checkout@v4
383
+ with:
384
+ submodules: false
385
+
386
+ - name: Checkout submodules (retry)
387
+ run: |
388
+ set -euo pipefail
389
+ git submodule sync --recursive
390
+ for attempt in 1 2 3 4 5; do
391
+ if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
392
+ exit 0
393
+ fi
394
+ echo "Submodule update failed (attempt $attempt/5). Retrying…"
395
+ sleep $((attempt * 10))
396
+ done
397
+ exit 1
398
+
399
+ - name: Select Xcode 26.1
400
+ run: |
401
+ sudo xcode-select -s /Applications/Xcode_26.1.app
402
+ xcodebuild -version
403
+
404
+ - name: Install XcodeGen / SwiftLint / SwiftFormat
405
+ run: |
406
+ brew install xcodegen swiftlint swiftformat
407
+
408
+ - name: Show toolchain
409
+ run: |
410
+ sw_vers
411
+ xcodebuild -version
412
+ swift --version
413
+
414
+ - name: Run ${{ matrix.task }}
415
+ run: ${{ matrix.command }}
416
+ ios:
417
+ if: false # ignore iOS in CI for now
418
+ runs-on: macos-latest
419
+ steps:
420
+ - name: Checkout
421
+ uses: actions/checkout@v4
422
+ with:
423
+ submodules: false
424
+
425
+ - name: Checkout submodules (retry)
426
+ run: |
427
+ set -euo pipefail
428
+ git submodule sync --recursive
429
+ for attempt in 1 2 3 4 5; do
430
+ if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
431
+ exit 0
432
+ fi
433
+ echo "Submodule update failed (attempt $attempt/5). Retrying…"
434
+ sleep $((attempt * 10))
435
+ done
436
+ exit 1
437
+
438
+ - name: Select Xcode 26.1
439
+ run: |
440
+ sudo xcode-select -s /Applications/Xcode_26.1.app
441
+ xcodebuild -version
442
+
443
+ - name: Install XcodeGen
444
+ run: brew install xcodegen
445
+
446
+ - name: Install SwiftLint / SwiftFormat
447
+ run: brew install swiftlint swiftformat
448
+
449
+ - name: Show toolchain
450
+ run: |
451
+ sw_vers
452
+ xcodebuild -version
453
+ swift --version
454
+
455
+ - name: Generate iOS project
456
+ run: |
457
+ cd apps/ios
458
+ xcodegen generate
459
+
460
+ - name: iOS tests
461
+ run: |
462
+ set -euo pipefail
463
+ RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult"
464
+ DEST_ID="$(
465
+ python3 - <<'PY'
466
+ import json
467
+ import subprocess
468
+ import sys
469
+ import uuid
470
+
471
+ def sh(args: list[str]) -> str:
472
+ return subprocess.check_output(args, text=True).strip()
473
+
474
+ # Prefer an already-created iPhone simulator if it exists.
475
+ devices = json.loads(sh(["xcrun", "simctl", "list", "devices", "-j"]))
476
+ candidates: list[tuple[str, str]] = []
477
+ for runtime, devs in (devices.get("devices") or {}).items():
478
+ for dev in devs or []:
479
+ if not dev.get("isAvailable"):
480
+ continue
481
+ name = str(dev.get("name") or "")
482
+ udid = str(dev.get("udid") or "")
483
+ if not udid or not name.startswith("iPhone"):
484
+ continue
485
+ candidates.append((name, udid))
486
+
487
+ candidates.sort(key=lambda it: (0 if "iPhone 16" in it[0] else 1, it[0]))
488
+ if candidates:
489
+ print(candidates[0][1])
490
+ sys.exit(0)
491
+
492
+ # Otherwise, create one from the newest available iOS runtime.
493
+ runtimes = json.loads(sh(["xcrun", "simctl", "list", "runtimes", "-j"])).get("runtimes") or []
494
+ ios = [rt for rt in runtimes if rt.get("platform") == "iOS" and rt.get("isAvailable")]
495
+ if not ios:
496
+ print("No available iOS runtimes found.", file=sys.stderr)
497
+ sys.exit(1)
498
+
499
+ def version_key(rt: dict) -> tuple[int, ...]:
500
+ parts: list[int] = []
501
+ for p in str(rt.get("version") or "0").split("."):
502
+ try:
503
+ parts.append(int(p))
504
+ except ValueError:
505
+ parts.append(0)
506
+ return tuple(parts)
507
+
508
+ ios.sort(key=version_key, reverse=True)
509
+ runtime = ios[0]
510
+ runtime_id = str(runtime.get("identifier") or "")
511
+ if not runtime_id:
512
+ print("Missing iOS runtime identifier.", file=sys.stderr)
513
+ sys.exit(1)
514
+
515
+ supported = runtime.get("supportedDeviceTypes") or []
516
+ iphones = [dt for dt in supported if dt.get("productFamily") == "iPhone"]
517
+ if not iphones:
518
+ print("No iPhone device types for iOS runtime.", file=sys.stderr)
519
+ sys.exit(1)
520
+
521
+ iphones.sort(
522
+ key=lambda dt: (
523
+ 0 if "iPhone 16" in str(dt.get("name") or "") else 1,
524
+ str(dt.get("name") or ""),
525
+ )
526
+ )
527
+ device_type_id = str(iphones[0].get("identifier") or "")
528
+ if not device_type_id:
529
+ print("Missing iPhone device type identifier.", file=sys.stderr)
530
+ sys.exit(1)
531
+
532
+ sim_name = f"CI iPhone {uuid.uuid4().hex[:8]}"
533
+ udid = sh(["xcrun", "simctl", "create", sim_name, device_type_id, runtime_id])
534
+ if not udid:
535
+ print("Failed to create iPhone simulator.", file=sys.stderr)
536
+ sys.exit(1)
537
+ print(udid)
538
+ PY
539
+ )"
540
+ echo "Using iOS Simulator id: $DEST_ID"
541
+ xcodebuild test \
542
+ -project apps/ios/Clawdis.xcodeproj \
543
+ -scheme Clawdis \
544
+ -destination "platform=iOS Simulator,id=$DEST_ID" \
545
+ -resultBundlePath "$RESULT_BUNDLE_PATH" \
546
+ -enableCodeCoverage YES
547
+
548
+ - name: iOS coverage summary
549
+ run: |
550
+ set -euo pipefail
551
+ RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult"
552
+ xcrun xccov view --report --only-targets "$RESULT_BUNDLE_PATH"
553
+
554
+ - name: iOS coverage gate (43%)
555
+ run: |
556
+ set -euo pipefail
557
+ RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult"
558
+ RESULT_BUNDLE_PATH="$RESULT_BUNDLE_PATH" python3 - <<'PY'
559
+ import json
560
+ import os
561
+ import subprocess
562
+ import sys
563
+
564
+ target_name = "Clawdis.app"
565
+ minimum = 0.43
566
+
567
+ report = json.loads(
568
+ subprocess.check_output(
569
+ ["xcrun", "xccov", "view", "--report", "--json", os.environ["RESULT_BUNDLE_PATH"]],
570
+ text=True,
571
+ )
572
+ )
573
+
574
+ target_coverage = None
575
+ for target in report.get("targets", []):
576
+ if target.get("name") == target_name:
577
+ target_coverage = float(target["lineCoverage"])
578
+ break
579
+
580
+ if target_coverage is None:
581
+ print(f"Could not find coverage for target: {target_name}")
582
+ sys.exit(1)
583
+
584
+ print(f"{target_name} line coverage: {target_coverage * 100:.2f}% (min {minimum * 100:.2f}%)")
585
+ if target_coverage + 1e-12 < minimum:
586
+ sys.exit(1)
587
+ PY
588
+
589
+ android:
590
+ runs-on: blacksmith-4vcpu-ubuntu-2404
591
+ strategy:
592
+ fail-fast: false
593
+ matrix:
594
+ include:
595
+ - task: test
596
+ command: ./gradlew --no-daemon :app:testDebugUnitTest
597
+ - task: build
598
+ command: ./gradlew --no-daemon :app:assembleDebug
599
+ steps:
600
+ - name: Checkout
601
+ uses: actions/checkout@v4
602
+ with:
603
+ submodules: false
604
+
605
+ - name: Checkout submodules (retry)
606
+ run: |
607
+ set -euo pipefail
608
+ git submodule sync --recursive
609
+ for attempt in 1 2 3 4 5; do
610
+ if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
611
+ exit 0
612
+ fi
613
+ echo "Submodule update failed (attempt $attempt/5). Retrying…"
614
+ sleep $((attempt * 10))
615
+ done
616
+ exit 1
617
+
618
+ - name: Setup Java
619
+ uses: actions/setup-java@v4
620
+ with:
621
+ distribution: temurin
622
+ java-version: 21
623
+
624
+ - name: Setup Android SDK
625
+ uses: android-actions/setup-android@v3
626
+ with:
627
+ accept-android-sdk-licenses: false
628
+
629
+ - name: Setup Gradle
630
+ uses: gradle/actions/setup-gradle@v4
631
+ with:
632
+ gradle-version: 8.11.1
633
+
634
+ - name: Install Android SDK packages
635
+ run: |
636
+ yes | sdkmanager --licenses >/dev/null
637
+ sdkmanager --install \
638
+ "platform-tools" \
639
+ "platforms;android-36" \
640
+ "build-tools;36.0.0"
641
+
642
+ - name: Run Android ${{ matrix.task }}
643
+ working-directory: apps/android
644
+ run: ${{ matrix.command }}
.github/workflows/docker-release.yml ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ tags:
8
+ - "v*"
9
+
10
+ env:
11
+ REGISTRY: ghcr.io
12
+ IMAGE_NAME: ${{ github.repository }}
13
+
14
+ jobs:
15
+ # Build amd64 image
16
+ build-amd64:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ packages: write
20
+ contents: read
21
+ outputs:
22
+ image-digest: ${{ steps.build.outputs.digest }}
23
+ image-metadata: ${{ steps.meta.outputs.json }}
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v4
27
+
28
+ - name: Set up Docker Buildx
29
+ uses: docker/setup-buildx-action@v3
30
+
31
+ - name: Login to GitHub Container Registry
32
+ uses: docker/login-action@v3
33
+ with:
34
+ registry: ${{ env.REGISTRY }}
35
+ username: ${{ github.repository_owner }}
36
+ password: ${{ secrets.GITHUB_TOKEN }}
37
+
38
+ - name: Extract metadata
39
+ id: meta
40
+ uses: docker/metadata-action@v5
41
+ with:
42
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
43
+ tags: |
44
+ type=ref,event=branch
45
+ type=semver,pattern={{version}}
46
+ type=semver,pattern={{version}},suffix=-amd64
47
+ type=semver,pattern={{version}},suffix=-arm64
48
+ type=ref,event=branch,suffix=-amd64
49
+ type=ref,event=branch,suffix=-arm64
50
+
51
+ - name: Build and push amd64 image
52
+ id: build
53
+ uses: docker/build-push-action@v6
54
+ with:
55
+ context: .
56
+ platforms: linux/amd64
57
+ labels: ${{ steps.meta.outputs.labels }}
58
+ tags: ${{ steps.meta.outputs.tags }}
59
+ cache-from: type=gha
60
+ cache-to: type=gha,mode=max
61
+ provenance: false
62
+ push: true
63
+
64
+ # Build arm64 image
65
+ build-arm64:
66
+ runs-on: ubuntu-24.04-arm
67
+ permissions:
68
+ packages: write
69
+ contents: read
70
+ outputs:
71
+ image-digest: ${{ steps.build.outputs.digest }}
72
+ image-metadata: ${{ steps.meta.outputs.json }}
73
+ steps:
74
+ - name: Checkout
75
+ uses: actions/checkout@v4
76
+
77
+ - name: Set up Docker Buildx
78
+ uses: docker/setup-buildx-action@v3
79
+
80
+ - name: Login to GitHub Container Registry
81
+ uses: docker/login-action@v3
82
+ with:
83
+ registry: ${{ env.REGISTRY }}
84
+ username: ${{ github.repository_owner }}
85
+ password: ${{ secrets.GITHUB_TOKEN }}
86
+
87
+ - name: Extract metadata
88
+ id: meta
89
+ uses: docker/metadata-action@v5
90
+ with:
91
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
92
+ tags: |
93
+ type=ref,event=branch
94
+ type=semver,pattern={{version}}
95
+ type=semver,pattern={{version}},suffix=-amd64
96
+ type=semver,pattern={{version}},suffix=-arm64
97
+ type=ref,event=branch,suffix=-amd64
98
+ type=ref,event=branch,suffix=-arm64
99
+
100
+ - name: Build and push arm64 image
101
+ id: build
102
+ uses: docker/build-push-action@v6
103
+ with:
104
+ context: .
105
+ platforms: linux/arm64
106
+ labels: ${{ steps.meta.outputs.labels }}
107
+ tags: ${{ steps.meta.outputs.tags }}
108
+ cache-from: type=gha
109
+ cache-to: type=gha,mode=max
110
+ provenance: false
111
+ push: true
112
+
113
+ # Create multi-platform manifest
114
+ create-manifest:
115
+ runs-on: ubuntu-latest
116
+ permissions:
117
+ packages: write
118
+ contents: read
119
+ needs: [build-amd64, build-arm64]
120
+ steps:
121
+ - name: Login to GitHub Container Registry
122
+ uses: docker/login-action@v3
123
+ with:
124
+ registry: ${{ env.REGISTRY }}
125
+ username: ${{ github.repository_owner }}
126
+ password: ${{ secrets.GITHUB_TOKEN }}
127
+
128
+ - name: Extract metadata for manifest
129
+ id: meta
130
+ uses: docker/metadata-action@v5
131
+ with:
132
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
133
+ tags: |
134
+ type=ref,event=branch
135
+ type=semver,pattern={{version}}
136
+
137
+ - name: Create and push manifest
138
+ run: |
139
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
140
+ ${{ needs.build-amd64.outputs.image-digest }} \
141
+ ${{ needs.build-arm64.outputs.image-digest }}
142
+ env:
143
+ DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }}
.github/workflows/formal-conformance.yml ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Formal models (informational conformance)
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ jobs:
7
+ formal_conformance:
8
+ runs-on: ubuntu-latest
9
+ timeout-minutes: 20
10
+ permissions:
11
+ contents: read
12
+ pull-requests: write
13
+
14
+ steps:
15
+ - name: Checkout openclaw (PR)
16
+ uses: actions/checkout@v4
17
+ with:
18
+ path: openclaw
19
+
20
+ - name: Checkout formal models
21
+ uses: actions/checkout@v4
22
+ with:
23
+ repository: vignesh07/clawdbot-formal-models
24
+ ref: main
25
+ path: clawdbot-formal-models
26
+
27
+ - name: Setup Node
28
+ uses: actions/setup-node@v4
29
+ with:
30
+ node-version: "22"
31
+
32
+ - name: Regenerate extracted constants from openclaw
33
+ run: |
34
+ set -euo pipefail
35
+ cd clawdbot-formal-models
36
+ export OPENCLAW_REPO_DIR="${GITHUB_WORKSPACE}/openclaw"
37
+ node scripts/extract-tool-groups.mjs
38
+ node scripts/check-tool-group-alias.mjs
39
+
40
+ - name: Model check (green suite)
41
+ run: |
42
+ set -euo pipefail
43
+ cd clawdbot-formal-models
44
+ make \
45
+ precedence groups elevated nodes-policy \
46
+ attacker approvals approvals-token nodes-pipeline \
47
+ gateway-exposure gateway-exposure-v2 gateway-exposure-v2-protected \
48
+ gateway-auth-conformance gateway-auth-tailscale gateway-auth-proxy \
49
+ pairing pairing-cap pairing-idempotency pairing-refresh pairing-refresh-race \
50
+ ingress-gating ingress-idempotency ingress-dedupe-fallback ingress-trace ingress-trace2 \
51
+ routing-isolation routing-precedence routing-identitylinks routing-identity-transitive routing-identity-symmetry routing-identity-channel-override \
52
+ routing-thread-parent discord-pluralkit \
53
+ ingress-retry session-key-stability session-explosion-bound config-normalization \
54
+ group-alias-check
55
+
56
+ - name: Model check (negative suite, expected violations)
57
+ continue-on-error: true
58
+ run: |
59
+ set -euo pipefail
60
+ cd clawdbot-formal-models
61
+ make -k \
62
+ precedence-negative groups-negative elevated-negative nodes-policy-negative \
63
+ attacker-negative attacker-nodes-negative attacker-nodes-allowlist-negative attacker-nodes-allowlist-negative \
64
+ approvals-negative approvals-token-negative nodes-pipeline-negative \
65
+ gateway-exposure-negative gateway-exposure-v2-negative gateway-exposure-v2-protected-negative \
66
+ gateway-exposure-v2-unsafe-custom gateway-exposure-v2-unsafe-tailnet gateway-exposure-v2-unsafe-auto \
67
+ gateway-auth-conformance-negative gateway-auth-tailscale-negative gateway-auth-proxy-negative \
68
+ pairing-negative pairing-cap-negative pairing-idempotency-negative pairing-refresh-negative pairing-refresh-race-negative \
69
+ ingress-gating-negative ingress-idempotency-negative ingress-dedupe-fallback-negative ingress-trace-negative ingress-trace2-negative \
70
+ routing-isolation-negative routing-precedence-negative routing-identitylinks-negative routing-identity-transitive-negative routing-identity-symmetry-negative routing-identity-channel-override-negative \
71
+ routing-thread-parent-negative discord-pluralkit-negative \
72
+ ingress-retry-negative session-key-stability-negative config-normalization-negative
73
+
74
+ - name: Compute drift
75
+ id: drift
76
+ run: |
77
+ set -euo pipefail
78
+ cd clawdbot-formal-models
79
+
80
+ if git diff --quiet; then
81
+ echo "drift=false" >> "$GITHUB_OUTPUT"
82
+ exit 0
83
+ fi
84
+
85
+ echo "drift=true" >> "$GITHUB_OUTPUT"
86
+ git diff > "${GITHUB_WORKSPACE}/formal-models-drift.diff"
87
+
88
+ - name: Upload drift diff artifact
89
+ if: steps.drift.outputs.drift == 'true'
90
+ uses: actions/upload-artifact@v4
91
+ with:
92
+ name: formal-models-conformance-drift
93
+ path: formal-models-drift.diff
94
+
95
+ - name: Comment on PR (informational)
96
+ if: steps.drift.outputs.drift == 'true'
97
+ uses: actions/github-script@v7
98
+ with:
99
+ script: |
100
+ const body = [
101
+ '⚠️ **Formal models conformance drift detected**',
102
+ '',
103
+ 'The formal models extracted constants (`generated/*`) do not match this openclaw PR.',
104
+ '',
105
+ 'This check is **informational** (not blocking merges yet).',
106
+ 'See the `formal-models-conformance-drift` artifact for the diff.',
107
+ '',
108
+ 'If this change is intentional, follow up by updating the formal models repo or regenerating the extracted artifacts there.',
109
+ ].join('\n');
110
+
111
+ await github.rest.issues.createComment({
112
+ owner: context.repo.owner,
113
+ repo: context.repo.repo,
114
+ issue_number: context.payload.pull_request.number,
115
+ body,
116
+ });
117
+
118
+ - name: Summary
119
+ run: |
120
+ if [ "${{ steps.drift.outputs.drift }}" = "true" ]; then
121
+ echo "Formal conformance drift detected (informational)."
122
+ else
123
+ echo "Formal conformance: no drift."
124
+ fi
.github/workflows/install-smoke.yml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Install Smoke
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ install-smoke:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout CLI
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Setup pnpm (corepack retry)
17
+ run: |
18
+ set -euo pipefail
19
+ corepack enable
20
+ for attempt in 1 2 3; do
21
+ if corepack prepare pnpm@10.23.0 --activate; then
22
+ pnpm -v
23
+ exit 0
24
+ fi
25
+ echo "corepack prepare failed (attempt $attempt/3). Retrying..."
26
+ sleep $((attempt * 10))
27
+ done
28
+ exit 1
29
+
30
+ - name: Install pnpm deps (minimal)
31
+ run: pnpm install --ignore-scripts --frozen-lockfile
32
+
33
+ - name: Run installer docker tests
34
+ env:
35
+ CLAWDBOT_INSTALL_URL: https://openclaw.ai/install.sh
36
+ CLAWDBOT_INSTALL_CLI_URL: https://openclaw.ai/install-cli.sh
37
+ CLAWDBOT_NO_ONBOARD: "1"
38
+ CLAWDBOT_INSTALL_SMOKE_SKIP_CLI: "1"
39
+ CLAWDBOT_INSTALL_SMOKE_SKIP_NONROOT: ${{ github.event_name == 'pull_request' && '1' || '0' }}
40
+ CLAWDBOT_INSTALL_SMOKE_SKIP_PREVIOUS: "1"
41
+ run: pnpm test:install:smoke
.github/workflows/labeler.yml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Labeler
2
+
3
+ on:
4
+ pull_request_target:
5
+ types: [opened, synchronize, reopened]
6
+
7
+ permissions: {}
8
+
9
+ jobs:
10
+ label:
11
+ permissions:
12
+ contents: read
13
+ pull-requests: write
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
17
+ id: app-token
18
+ with:
19
+ app-id: "2729701"
20
+ private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
21
+ - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
22
+ with:
23
+ configuration-path: .github/labeler.yml
24
+ repo-token: ${{ steps.app-token.outputs.token }}
25
+ sync-labels: true
.github/workflows/workflow-sanity.yml ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Workflow Sanity
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+
7
+ jobs:
8
+ no-tabs:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Checkout
12
+ uses: actions/checkout@v4
13
+
14
+ - name: Fail on tabs in workflow files
15
+ run: |
16
+ python - <<'PY'
17
+ from __future__ import annotations
18
+
19
+ import pathlib
20
+ import sys
21
+
22
+ root = pathlib.Path(".github/workflows")
23
+ bad: list[str] = []
24
+ for path in sorted(root.rglob("*.yml")):
25
+ if b"\t" in path.read_bytes():
26
+ bad.append(str(path))
27
+
28
+ for path in sorted(root.rglob("*.yaml")):
29
+ if b"\t" in path.read_bytes():
30
+ bad.append(str(path))
31
+
32
+ if bad:
33
+ print("Tabs found in workflow file(s):")
34
+ for path in bad:
35
+ print(f"- {path}")
36
+ sys.exit(1)
37
+ PY
.pi/extensions/diff.ts ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Diff Extension
3
+ *
4
+ * /diff command shows modified/deleted/new files from git status and opens
5
+ * the selected file in VS Code's diff view.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
10
+ import {
11
+ Container,
12
+ Key,
13
+ matchesKey,
14
+ type SelectItem,
15
+ SelectList,
16
+ Text,
17
+ } from "@mariozechner/pi-tui";
18
+
19
+ interface FileInfo {
20
+ status: string;
21
+ statusLabel: string;
22
+ file: string;
23
+ }
24
+
25
+ export default function (pi: ExtensionAPI) {
26
+ pi.registerCommand("diff", {
27
+ description: "Show git changes and open in VS Code diff view",
28
+ handler: async (_args, ctx) => {
29
+ if (!ctx.hasUI) {
30
+ ctx.ui.notify("No UI available", "error");
31
+ return;
32
+ }
33
+
34
+ // Get changed files from git status
35
+ const result = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
36
+
37
+ if (result.code !== 0) {
38
+ ctx.ui.notify(`git status failed: ${result.stderr}`, "error");
39
+ return;
40
+ }
41
+
42
+ if (!result.stdout || !result.stdout.trim()) {
43
+ ctx.ui.notify("No changes in working tree", "info");
44
+ return;
45
+ }
46
+
47
+ // Parse git status output
48
+ // Format: XY filename (where XY is two-letter status, then space, then filename)
49
+ const lines = result.stdout.split("\n");
50
+ const files: FileInfo[] = [];
51
+
52
+ for (const line of lines) {
53
+ if (line.length < 4) {
54
+ continue;
55
+ } // Need at least "XY f"
56
+
57
+ const status = line.slice(0, 2);
58
+ const file = line.slice(2).trimStart();
59
+
60
+ // Translate status codes to short labels
61
+ let statusLabel: string;
62
+ if (status.includes("M")) {
63
+ statusLabel = "M";
64
+ } else if (status.includes("A")) {
65
+ statusLabel = "A";
66
+ } else if (status.includes("D")) {
67
+ statusLabel = "D";
68
+ } else if (status.includes("?")) {
69
+ statusLabel = "?";
70
+ } else if (status.includes("R")) {
71
+ statusLabel = "R";
72
+ } else if (status.includes("C")) {
73
+ statusLabel = "C";
74
+ } else {
75
+ statusLabel = status.trim() || "~";
76
+ }
77
+
78
+ files.push({ status: statusLabel, statusLabel, file });
79
+ }
80
+
81
+ if (files.length === 0) {
82
+ ctx.ui.notify("No changes found", "info");
83
+ return;
84
+ }
85
+
86
+ const openSelected = async (fileInfo: FileInfo): Promise<void> => {
87
+ try {
88
+ // Open in VS Code diff view.
89
+ // For untracked files, git difftool won't work, so fall back to just opening the file.
90
+ if (fileInfo.status === "?") {
91
+ await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd });
92
+ return;
93
+ }
94
+
95
+ const diffResult = await pi.exec(
96
+ "git",
97
+ ["difftool", "-y", "--tool=vscode", fileInfo.file],
98
+ {
99
+ cwd: ctx.cwd,
100
+ },
101
+ );
102
+ if (diffResult.code !== 0) {
103
+ await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd });
104
+ }
105
+ } catch (error) {
106
+ const message = error instanceof Error ? error.message : String(error);
107
+ ctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, "error");
108
+ }
109
+ };
110
+
111
+ // Show file picker with SelectList
112
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
113
+ const container = new Container();
114
+
115
+ // Top border
116
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
117
+
118
+ // Title
119
+ container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to diff")), 0, 0));
120
+
121
+ // Build select items with colored status
122
+ const items: SelectItem[] = files.map((f) => {
123
+ let statusColor: string;
124
+ switch (f.status) {
125
+ case "M":
126
+ statusColor = theme.fg("warning", f.status);
127
+ break;
128
+ case "A":
129
+ statusColor = theme.fg("success", f.status);
130
+ break;
131
+ case "D":
132
+ statusColor = theme.fg("error", f.status);
133
+ break;
134
+ case "?":
135
+ statusColor = theme.fg("muted", f.status);
136
+ break;
137
+ default:
138
+ statusColor = theme.fg("dim", f.status);
139
+ }
140
+ return {
141
+ value: f,
142
+ label: `${statusColor} ${f.file}`,
143
+ };
144
+ });
145
+
146
+ const visibleRows = Math.min(files.length, 15);
147
+ let currentIndex = 0;
148
+
149
+ const selectList = new SelectList(items, visibleRows, {
150
+ selectedPrefix: (t) => theme.fg("accent", t),
151
+ selectedText: (t) => t, // Keep existing colors
152
+ description: (t) => theme.fg("muted", t),
153
+ scrollInfo: (t) => theme.fg("dim", t),
154
+ noMatch: (t) => theme.fg("warning", t),
155
+ });
156
+ selectList.onSelect = (item) => {
157
+ void openSelected(item.value as FileInfo);
158
+ };
159
+ selectList.onCancel = () => done();
160
+ selectList.onSelectionChange = (item) => {
161
+ currentIndex = items.indexOf(item);
162
+ };
163
+ container.addChild(selectList);
164
+
165
+ // Help text
166
+ container.addChild(
167
+ new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
168
+ );
169
+
170
+ // Bottom border
171
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
172
+
173
+ return {
174
+ render: (w) => container.render(w),
175
+ invalidate: () => container.invalidate(),
176
+ handleInput: (data) => {
177
+ // Add paging with left/right
178
+ if (matchesKey(data, Key.left)) {
179
+ // Page up - clamp to 0
180
+ currentIndex = Math.max(0, currentIndex - visibleRows);
181
+ selectList.setSelectedIndex(currentIndex);
182
+ } else if (matchesKey(data, Key.right)) {
183
+ // Page down - clamp to last
184
+ currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
185
+ selectList.setSelectedIndex(currentIndex);
186
+ } else {
187
+ selectList.handleInput(data);
188
+ }
189
+ tui.requestRender();
190
+ },
191
+ };
192
+ });
193
+ },
194
+ });
195
+ }
.pi/extensions/files.ts ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Files Extension
3
+ *
4
+ * /files command lists all files the model has read/written/edited in the active session branch,
5
+ * coalesced by path and sorted newest first. Selecting a file opens it in VS Code.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
10
+ import {
11
+ Container,
12
+ Key,
13
+ matchesKey,
14
+ type SelectItem,
15
+ SelectList,
16
+ Text,
17
+ } from "@mariozechner/pi-tui";
18
+
19
+ interface FileEntry {
20
+ path: string;
21
+ operations: Set<"read" | "write" | "edit">;
22
+ lastTimestamp: number;
23
+ }
24
+
25
+ type FileToolName = "read" | "write" | "edit";
26
+
27
+ export default function (pi: ExtensionAPI) {
28
+ pi.registerCommand("files", {
29
+ description: "Show files read/written/edited in this session",
30
+ handler: async (_args, ctx) => {
31
+ if (!ctx.hasUI) {
32
+ ctx.ui.notify("No UI available", "error");
33
+ return;
34
+ }
35
+
36
+ // Get the current branch (path from leaf to root)
37
+ const branch = ctx.sessionManager.getBranch();
38
+
39
+ // First pass: collect tool calls (id -> {path, name}) from assistant messages
40
+ const toolCalls = new Map<string, { path: string; name: FileToolName; timestamp: number }>();
41
+
42
+ for (const entry of branch) {
43
+ if (entry.type !== "message") {
44
+ continue;
45
+ }
46
+ const msg = entry.message;
47
+
48
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
49
+ for (const block of msg.content) {
50
+ if (block.type === "toolCall") {
51
+ const name = block.name;
52
+ if (name === "read" || name === "write" || name === "edit") {
53
+ const path = block.arguments?.path;
54
+ if (path && typeof path === "string") {
55
+ toolCalls.set(block.id, { path, name, timestamp: msg.timestamp });
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ // Second pass: match tool results to get the actual execution timestamp
64
+ const fileMap = new Map<string, FileEntry>();
65
+
66
+ for (const entry of branch) {
67
+ if (entry.type !== "message") {
68
+ continue;
69
+ }
70
+ const msg = entry.message;
71
+
72
+ if (msg.role === "toolResult") {
73
+ const toolCall = toolCalls.get(msg.toolCallId);
74
+ if (!toolCall) {
75
+ continue;
76
+ }
77
+
78
+ const { path, name } = toolCall;
79
+ const timestamp = msg.timestamp;
80
+
81
+ const existing = fileMap.get(path);
82
+ if (existing) {
83
+ existing.operations.add(name);
84
+ if (timestamp > existing.lastTimestamp) {
85
+ existing.lastTimestamp = timestamp;
86
+ }
87
+ } else {
88
+ fileMap.set(path, {
89
+ path,
90
+ operations: new Set([name]),
91
+ lastTimestamp: timestamp,
92
+ });
93
+ }
94
+ }
95
+ }
96
+
97
+ if (fileMap.size === 0) {
98
+ ctx.ui.notify("No files read/written/edited in this session", "info");
99
+ return;
100
+ }
101
+
102
+ // Sort by most recent first
103
+ const files = Array.from(fileMap.values()).toSorted(
104
+ (a, b) => b.lastTimestamp - a.lastTimestamp,
105
+ );
106
+
107
+ const openSelected = async (file: FileEntry): Promise<void> => {
108
+ try {
109
+ await pi.exec("code", ["-g", file.path], { cwd: ctx.cwd });
110
+ } catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ ctx.ui.notify(`Failed to open ${file.path}: ${message}`, "error");
113
+ }
114
+ };
115
+
116
+ // Show file picker with SelectList
117
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
118
+ const container = new Container();
119
+
120
+ // Top border
121
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
122
+
123
+ // Title
124
+ container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to open")), 0, 0));
125
+
126
+ // Build select items with colored operations
127
+ const items: SelectItem[] = files.map((f) => {
128
+ const ops: string[] = [];
129
+ if (f.operations.has("read")) {
130
+ ops.push(theme.fg("muted", "R"));
131
+ }
132
+ if (f.operations.has("write")) {
133
+ ops.push(theme.fg("success", "W"));
134
+ }
135
+ if (f.operations.has("edit")) {
136
+ ops.push(theme.fg("warning", "E"));
137
+ }
138
+ const opsLabel = ops.join("");
139
+ return {
140
+ value: f,
141
+ label: `${opsLabel} ${f.path}`,
142
+ };
143
+ });
144
+
145
+ const visibleRows = Math.min(files.length, 15);
146
+ let currentIndex = 0;
147
+
148
+ const selectList = new SelectList(items, visibleRows, {
149
+ selectedPrefix: (t) => theme.fg("accent", t),
150
+ selectedText: (t) => t, // Keep existing colors
151
+ description: (t) => theme.fg("muted", t),
152
+ scrollInfo: (t) => theme.fg("dim", t),
153
+ noMatch: (t) => theme.fg("warning", t),
154
+ });
155
+ selectList.onSelect = (item) => {
156
+ void openSelected(item.value as FileEntry);
157
+ };
158
+ selectList.onCancel = () => done();
159
+ selectList.onSelectionChange = (item) => {
160
+ currentIndex = items.indexOf(item);
161
+ };
162
+ container.addChild(selectList);
163
+
164
+ // Help text
165
+ container.addChild(
166
+ new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
167
+ );
168
+
169
+ // Bottom border
170
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
171
+
172
+ return {
173
+ render: (w) => container.render(w),
174
+ invalidate: () => container.invalidate(),
175
+ handleInput: (data) => {
176
+ // Add paging with left/right
177
+ if (matchesKey(data, Key.left)) {
178
+ // Page up - clamp to 0
179
+ currentIndex = Math.max(0, currentIndex - visibleRows);
180
+ selectList.setSelectedIndex(currentIndex);
181
+ } else if (matchesKey(data, Key.right)) {
182
+ // Page down - clamp to last
183
+ currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
184
+ selectList.setSelectedIndex(currentIndex);
185
+ } else {
186
+ selectList.handleInput(data);
187
+ }
188
+ tui.requestRender();
189
+ },
190
+ };
191
+ });
192
+ },
193
+ });
194
+ }
.pi/extensions/prompt-url-widget.ts ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ DynamicBorder,
3
+ type ExtensionAPI,
4
+ type ExtensionContext,
5
+ } from "@mariozechner/pi-coding-agent";
6
+ import { Container, Text } from "@mariozechner/pi-tui";
7
+
8
+ const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im;
9
+ const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im;
10
+
11
+ type PromptMatch = {
12
+ kind: "pr" | "issue";
13
+ url: string;
14
+ };
15
+
16
+ type GhMetadata = {
17
+ title?: string;
18
+ author?: {
19
+ login?: string;
20
+ name?: string | null;
21
+ };
22
+ };
23
+
24
+ function extractPromptMatch(prompt: string): PromptMatch | undefined {
25
+ const prMatch = prompt.match(PR_PROMPT_PATTERN);
26
+ if (prMatch?.[1]) {
27
+ return { kind: "pr", url: prMatch[1].trim() };
28
+ }
29
+
30
+ const issueMatch = prompt.match(ISSUE_PROMPT_PATTERN);
31
+ if (issueMatch?.[1]) {
32
+ return { kind: "issue", url: issueMatch[1].trim() };
33
+ }
34
+
35
+ return undefined;
36
+ }
37
+
38
+ async function fetchGhMetadata(
39
+ pi: ExtensionAPI,
40
+ kind: PromptMatch["kind"],
41
+ url: string,
42
+ ): Promise<GhMetadata | undefined> {
43
+ const args =
44
+ kind === "pr"
45
+ ? ["pr", "view", url, "--json", "title,author"]
46
+ : ["issue", "view", url, "--json", "title,author"];
47
+
48
+ try {
49
+ const result = await pi.exec("gh", args);
50
+ if (result.code !== 0 || !result.stdout) {
51
+ return undefined;
52
+ }
53
+ return JSON.parse(result.stdout) as GhMetadata;
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ }
58
+
59
+ function formatAuthor(author?: GhMetadata["author"]): string | undefined {
60
+ if (!author) {
61
+ return undefined;
62
+ }
63
+ const name = author.name?.trim();
64
+ const login = author.login?.trim();
65
+ if (name && login) {
66
+ return `${name} (@${login})`;
67
+ }
68
+ if (login) {
69
+ return `@${login}`;
70
+ }
71
+ if (name) {
72
+ return name;
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
78
+ const setWidget = (
79
+ ctx: ExtensionContext,
80
+ match: PromptMatch,
81
+ title?: string,
82
+ authorText?: string,
83
+ ) => {
84
+ ctx.ui.setWidget("prompt-url", (_tui, thm) => {
85
+ const titleText = title ? thm.fg("accent", title) : thm.fg("accent", match.url);
86
+ const authorLine = authorText ? thm.fg("muted", authorText) : undefined;
87
+ const urlLine = thm.fg("dim", match.url);
88
+
89
+ const lines = [titleText];
90
+ if (authorLine) {
91
+ lines.push(authorLine);
92
+ }
93
+ lines.push(urlLine);
94
+
95
+ const container = new Container();
96
+ container.addChild(new DynamicBorder((s: string) => thm.fg("muted", s)));
97
+ container.addChild(new Text(lines.join("\n"), 1, 0));
98
+ return container;
99
+ });
100
+ };
101
+
102
+ const applySessionName = (ctx: ExtensionContext, match: PromptMatch, title?: string) => {
103
+ const label = match.kind === "pr" ? "PR" : "Issue";
104
+ const trimmedTitle = title?.trim();
105
+ const fallbackName = `${label}: ${match.url}`;
106
+ const desiredName = trimmedTitle ? `${label}: ${trimmedTitle} (${match.url})` : fallbackName;
107
+ const currentName = pi.getSessionName()?.trim();
108
+ if (!currentName) {
109
+ pi.setSessionName(desiredName);
110
+ return;
111
+ }
112
+ if (currentName === match.url || currentName === fallbackName) {
113
+ pi.setSessionName(desiredName);
114
+ }
115
+ };
116
+
117
+ pi.on("before_agent_start", async (event, ctx) => {
118
+ if (!ctx.hasUI) {
119
+ return;
120
+ }
121
+ const match = extractPromptMatch(event.prompt);
122
+ if (!match) {
123
+ return;
124
+ }
125
+
126
+ setWidget(ctx, match);
127
+ applySessionName(ctx, match);
128
+ void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
129
+ const title = meta?.title?.trim();
130
+ const authorText = formatAuthor(meta?.author);
131
+ setWidget(ctx, match, title, authorText);
132
+ applySessionName(ctx, match, title);
133
+ });
134
+ });
135
+
136
+ pi.on("session_switch", async (_event, ctx) => {
137
+ rebuildFromSession(ctx);
138
+ });
139
+
140
+ const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => {
141
+ if (!content) {
142
+ return "";
143
+ }
144
+ if (typeof content === "string") {
145
+ return content;
146
+ }
147
+ return (
148
+ content
149
+ .filter((block): block is { type: "text"; text: string } => block.type === "text")
150
+ .map((block) => block.text)
151
+ .join("\n") ?? ""
152
+ );
153
+ };
154
+
155
+ const rebuildFromSession = (ctx: ExtensionContext) => {
156
+ if (!ctx.hasUI) {
157
+ return;
158
+ }
159
+
160
+ const entries = ctx.sessionManager.getEntries();
161
+ const lastMatch = [...entries].toReversed().find((entry) => {
162
+ if (entry.type !== "message" || entry.message.role !== "user") {
163
+ return false;
164
+ }
165
+ const text = getUserText(entry.message.content);
166
+ return !!extractPromptMatch(text);
167
+ });
168
+
169
+ const content =
170
+ lastMatch?.type === "message" && lastMatch.message.role === "user"
171
+ ? lastMatch.message.content
172
+ : undefined;
173
+ const text = getUserText(content);
174
+ const match = text ? extractPromptMatch(text) : undefined;
175
+ if (!match) {
176
+ ctx.ui.setWidget("prompt-url", undefined);
177
+ return;
178
+ }
179
+
180
+ setWidget(ctx, match);
181
+ applySessionName(ctx, match);
182
+ void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
183
+ const title = meta?.title?.trim();
184
+ const authorText = formatAuthor(meta?.author);
185
+ setWidget(ctx, match, title, authorText);
186
+ applySessionName(ctx, match, title);
187
+ });
188
+ };
189
+
190
+ pi.on("session_start", async (_event, ctx) => {
191
+ rebuildFromSession(ctx);
192
+ });
193
+ }
.pi/extensions/redraws.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Redraws Extension
3
+ *
4
+ * Exposes /tui to show TUI redraw stats.
5
+ */
6
+
7
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import { Text } from "@mariozechner/pi-tui";
9
+
10
+ export default function (pi: ExtensionAPI) {
11
+ pi.registerCommand("tui", {
12
+ description: "Show TUI stats",
13
+ handler: async (_args, ctx) => {
14
+ if (!ctx.hasUI) {
15
+ return;
16
+ }
17
+ let redraws = 0;
18
+ await ctx.ui.custom<void>((tui, _theme, _keybindings, done) => {
19
+ redraws = tui.fullRedraws;
20
+ done(undefined);
21
+ return new Text("", 0, 0);
22
+ });
23
+ ctx.ui.notify(`TUI full redraws: ${redraws}`, "info");
24
+ },
25
+ });
26
+ }
.pi/git/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *
2
+ !.gitignore
.pi/prompts/cl.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ description: Audit changelog entries before release
3
+ ---
4
+
5
+ Audit changelog entries for all commits since the last release.
6
+
7
+ ## Process
8
+
9
+ 1. **Find the last release tag:**
10
+
11
+ ```bash
12
+ git tag --sort=-version:refname | head -1
13
+ ```
14
+
15
+ 2. **List all commits since that tag:**
16
+
17
+ ```bash
18
+ git log <tag>..HEAD --oneline
19
+ ```
20
+
21
+ 3. **Read each package's [Unreleased] section:**
22
+ - packages/ai/CHANGELOG.md
23
+ - packages/tui/CHANGELOG.md
24
+ - packages/coding-agent/CHANGELOG.md
25
+
26
+ 4. **For each commit, check:**
27
+ - Skip: changelog updates, doc-only changes, release housekeeping
28
+ - Determine which package(s) the commit affects (use `git show <hash> --stat`)
29
+ - Verify a changelog entry exists in the affected package(s)
30
+ - For external contributions (PRs), verify format: `Description ([#N](url) by [@user](url))`
31
+
32
+ 5. **Cross-package duplication rule:**
33
+ Changes in `ai`, `agent` or `tui` that affect end users should be duplicated to `coding-agent` changelog, since coding-agent is the user-facing package that depends on them.
34
+
35
+ 6. **Add New Features section after changelog fixes:**
36
+ - Insert a `### New Features` section at the start of `## [Unreleased]` in `packages/coding-agent/CHANGELOG.md`.
37
+ - Propose the top new features to the user for confirmation before writing them.
38
+ - Link to relevant docs and sections whenever possible.
39
+
40
+ 7. **Report:**
41
+ - List commits with missing entries
42
+ - List entries that need cross-package duplication
43
+ - Add any missing entries directly
44
+
45
+ ## Changelog Format Reference
46
+
47
+ Sections (in order):
48
+
49
+ - `### Breaking Changes` - API changes requiring migration
50
+ - `### Added` - New features
51
+ - `### Changed` - Changes to existing functionality
52
+ - `### Fixed` - Bug fixes
53
+ - `### Removed` - Removed features
54
+
55
+ Attribution:
56
+
57
+ - Internal: `Fixed foo ([#123](https://github.com/badlogic/pi-mono/issues/123))`
58
+ - External: `Added bar ([#456](https://github.com/badlogic/pi-mono/pull/456) by [@user](https://github.com/user))`
.pi/prompts/is.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ description: Analyze GitHub issues (bugs or feature requests)
3
+ ---
4
+
5
+ Analyze GitHub issue(s): $ARGUMENTS
6
+
7
+ For each issue:
8
+
9
+ 1. Read the issue in full, including all comments and linked issues/PRs.
10
+
11
+ 2. **For bugs**:
12
+ - Ignore any root cause analysis in the issue (likely wrong)
13
+ - Read all related code files in full (no truncation)
14
+ - Trace the code path and identify the actual root cause
15
+ - Propose a fix
16
+
17
+ 3. **For feature requests**:
18
+ - Read all related code files in full (no truncation)
19
+ - Propose the most concise implementation approach
20
+ - List affected files and changes needed
21
+
22
+ Do NOT implement unless explicitly asked. Analyze and propose only.
.pi/prompts/landpr.md ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ description: Land a PR (merge with proper workflow)
3
+ ---
4
+
5
+ Input
6
+
7
+ - PR: $1 <number|url>
8
+ - If missing: use the most recent PR mentioned in the conversation.
9
+ - If ambiguous: ask.
10
+
11
+ Do (review-only)
12
+ Goal: produce a thorough review and a clear recommendation (READY for /landpr vs NEEDS WORK). Do NOT merge, do NOT push, do NOT make changes in the repo as part of this command.
13
+
14
+ 1. Identify PR meta + context
15
+
16
+ ```sh
17
+ gh pr view <PR> --json number,title,state,isDraft,author,baseRefName,headRefName,headRepository,url,body,labels,assignees,reviewRequests,files,additions,deletions --jq '{number,title,url,state,isDraft,author:.author.login,base:.baseRefName,head:.headRefName,headRepo:.headRepository.nameWithOwner,additions,deletions,files:.files|length}'
18
+ ```
19
+
20
+ 2. Read the PR description carefully
21
+ - Summarize the stated goal, scope, and any “why now?” rationale.
22
+ - Call out any missing context: motivation, alternatives considered, rollout/compat notes, risk.
23
+
24
+ 3. Read the diff thoroughly (prefer full diff)
25
+
26
+ ```sh
27
+ gh pr diff <PR>
28
+ # If you need more surrounding context for files:
29
+ gh pr checkout <PR> # optional; still review-only
30
+ git show --stat
31
+ ```
32
+
33
+ 4. Validate the change is needed / valuable
34
+ - What user/customer/dev pain does this solve?
35
+ - Is this change the smallest reasonable fix?
36
+ - Are we introducing complexity for marginal benefit?
37
+ - Are we changing behavior/contract in a way that needs docs or a release note?
38
+
39
+ 5. Evaluate implementation quality + optimality
40
+ - Correctness: edge cases, error handling, null/undefined, concurrency, ordering.
41
+ - Design: is the abstraction/architecture appropriate or over/under-engineered?
42
+ - Performance: hot paths, allocations, queries, network, N+1s, caching.
43
+ - Security/privacy: authz/authn, input validation, secrets, logging PII.
44
+ - Backwards compatibility: public APIs, config, migrations.
45
+ - Style consistency: formatting, naming, patterns used elsewhere.
46
+
47
+ 6. Tests & verification
48
+ - Identify what’s covered by tests (unit/integration/e2e).
49
+ - Are there regression tests for the bug fixed / scenario added?
50
+ - Missing tests? Call out exact cases that should be added.
51
+ - If tests are present, do they actually assert the important behavior (not just snapshots / happy path)?
52
+
53
+ 7. Follow-up refactors / cleanup suggestions
54
+ - Any code that should be simplified before merge?
55
+ - Any TODOs that should be tickets vs addressed now?
56
+ - Any deprecations, docs, types, or lint rules we should adjust?
57
+
58
+ 8. Key questions to answer explicitly
59
+ - Can we fix everything ourselves in a follow-up, or does the contributor need to update this PR?
60
+ - Any blocking concerns (must-fix before merge)?
61
+ - Is this PR ready to land, or does it need work?
62
+
63
+ 9. Output (structured)
64
+ Produce a review with these sections:
65
+
66
+ A) TL;DR recommendation
67
+
68
+ - One of: READY FOR /landpr | NEEDS WORK | NEEDS DISCUSSION
69
+ - 1–3 sentence rationale.
70
+
71
+ B) What changed
72
+
73
+ - Brief bullet summary of the diff/behavioral changes.
74
+
75
+ C) What’s good
76
+
77
+ - Bullets: correctness, simplicity, tests, docs, ergonomics, etc.
78
+
79
+ D) Concerns / questions (actionable)
80
+
81
+ - Numbered list.
82
+ - Mark each item as:
83
+ - BLOCKER (must fix before merge)
84
+ - IMPORTANT (should fix before merge)
85
+ - NIT (optional)
86
+ - For each: point to the file/area and propose a concrete fix or alternative.
87
+
88
+ E) Tests
89
+
90
+ - What exists.
91
+ - What’s missing (specific scenarios).
92
+
93
+ F) Follow-ups (optional)
94
+
95
+ - Non-blocking refactors/tickets to open later.
96
+
97
+ G) Suggested PR comment (optional)
98
+
99
+ - Offer: “Want me to draft a PR comment to the author?”
100
+ - If yes, provide a ready-to-paste comment summarizing the above, with clear asks.
101
+
102
+ Rules / Guardrails
103
+
104
+ - Review only: do not merge (`gh pr merge`), do not push branches, do not edit code.
105
+ - If you need clarification, ask questions rather than guessing.
.pi/prompts/reviewpr.md ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ description: Review a PR thoroughly without merging
3
+ ---
4
+
5
+ Input
6
+
7
+ - PR: $1 <number|url>
8
+ - If missing: use the most recent PR mentioned in the conversation.
9
+ - If ambiguous: ask.
10
+
11
+ Do (review-only)
12
+ Goal: produce a thorough review and a clear recommendation (READY for /landpr vs NEEDS WORK). Do NOT merge, do NOT push, do NOT make changes in the repo as part of this command.
13
+
14
+ 1. Identify PR meta + context
15
+
16
+ ```sh
17
+ gh pr view <PR> --json number,title,state,isDraft,author,baseRefName,headRefName,headRepository,url,body,labels,assignees,reviewRequests,files,additions,deletions --jq '{number,title,url,state,isDraft,author:.author.login,base:.baseRefName,head:.headRefName,headRepo:.headRepository.nameWithOwner,additions,deletions,files:.files|length}'
18
+ ```
19
+
20
+ 2. Read the PR description carefully
21
+ - Summarize the stated goal, scope, and any "why now?" rationale.
22
+ - Call out any missing context: motivation, alternatives considered, rollout/compat notes, risk.
23
+
24
+ 3. Read the diff thoroughly (prefer full diff)
25
+
26
+ ```sh
27
+ gh pr diff <PR>
28
+ # If you need more surrounding context for files:
29
+ gh pr checkout <PR> # optional; still review-only
30
+ git show --stat
31
+ ```
32
+
33
+ 4. Validate the change is needed / valuable
34
+ - What user/customer/dev pain does this solve?
35
+ - Is this change the smallest reasonable fix?
36
+ - Are we introducing complexity for marginal benefit?
37
+ - Are we changing behavior/contract in a way that needs docs or a release note?
38
+
39
+ 5. Evaluate implementation quality + optimality
40
+ - Correctness: edge cases, error handling, null/undefined, concurrency, ordering.
41
+ - Design: is the abstraction/architecture appropriate or over/under-engineered?
42
+ - Performance: hot paths, allocations, queries, network, N+1s, caching.
43
+ - Security/privacy: authz/authn, input validation, secrets, logging PII.
44
+ - Backwards compatibility: public APIs, config, migrations.
45
+ - Style consistency: formatting, naming, patterns used elsewhere.
46
+
47
+ 6. Tests & verification
48
+ - Identify what's covered by tests (unit/integration/e2e).
49
+ - Are there regression tests for the bug fixed / scenario added?
50
+ - Missing tests? Call out exact cases that should be added.
51
+ - If tests are present, do they actually assert the important behavior (not just snapshots / happy path)?
52
+
53
+ 7. Follow-up refactors / cleanup suggestions
54
+ - Any code that should be simplified before merge?
55
+ - Any TODOs that should be tickets vs addressed now?
56
+ - Any deprecations, docs, types, or lint rules we should adjust?
57
+
58
+ 8. Key questions to answer explicitly
59
+ - Can we fix everything ourselves in a follow-up, or does the contributor need to update this PR?
60
+ - Any blocking concerns (must-fix before merge)?
61
+ - Is this PR ready to land, or does it need work?
62
+
63
+ 9. Output (structured)
64
+ Produce a review with these sections:
65
+
66
+ A) TL;DR recommendation
67
+
68
+ - One of: READY FOR /landpr | NEEDS WORK | NEEDS DISCUSSION
69
+ - 1–3 sentence rationale.
70
+
71
+ B) What changed
72
+
73
+ - Brief bullet summary of the diff/behavioral changes.
74
+
75
+ C) What's good
76
+
77
+ - Bullets: correctness, simplicity, tests, docs, ergonomics, etc.
78
+
79
+ D) Concerns / questions (actionable)
80
+
81
+ - Numbered list.
82
+ - Mark each item as:
83
+ - BLOCKER (must fix before merge)
84
+ - IMPORTANT (should fix before merge)
85
+ - NIT (optional)
86
+ - For each: point to the file/area and propose a concrete fix or alternative.
87
+
88
+ E) Tests
89
+
90
+ - What exists.
91
+ - What's missing (specific scenarios).
92
+
93
+ F) Follow-ups (optional)
94
+
95
+ - Non-blocking refactors/tickets to open later.
96
+
97
+ G) Suggested PR comment (optional)
98
+
99
+ - Offer: "Want me to draft a PR comment to the author?"
100
+ - If yes, provide a ready-to-paste comment summarizing the above, with clear asks.
101
+
102
+ Rules / Guardrails
103
+
104
+ - Review only: do not merge (`gh pr merge`), do not push branches, do not edit code.
105
+ - If you need clarification, ask questions rather than guessing.