Skip to content

Commit aaafcef

Browse files
Merge main into fix/gitactions-hunt
2 parents 17d60a9 + 8c9af54 commit aaafcef

30,691 files changed

Lines changed: 2287634 additions & 1783331 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/test-t3-app/SKILL.md‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ description: Test T3 Code's web and desktop UI through its built-in Browser pane
55

66
# Test T3 web and desktop
77

8-
Use T3's built-in Browser panel for verification. If its tools are absent or
9-
the panel reports unavailable, explain the blocker and stop verification.
10-
Do not install or switch to another automation system. For native mobile
11-
testing, use [test-t3-mobile](../test-t3-mobile/SKILL.md).
8+
Use T3's built-in Browser panel for verification. Follow the recovery steps
9+
below before declaring it unavailable. Do not install or switch to another
10+
automation system. For native mobile testing, use
11+
[test-t3-mobile](../test-t3-mobile/SKILL.md).
1212

1313
## Start the app
1414

@@ -36,8 +36,13 @@ model, such as Haiku for Claude or Luna for Codex.
3636

3737
## Use the Browser panel
3838

39-
Call `preview_status`, then `preview_open` if the Browser panel is
40-
closed. Navigate to the complete startup pairing URL once with
39+
Call `preview_status`. If no tab can be automated, call `preview_open`, including
40+
when status reports `available: false` or `tabId: null`. Status can report
41+
unavailable simply because no tab exists yet; opening a tab needs no user action.
42+
Stop verification and explain the blocker only if the tools are absent or
43+
`preview_open` reports unavailable or fails.
44+
45+
Navigate to the complete startup pairing URL once with
4146
`preview_navigate`, then use `preview_snapshot` and T3's interaction tools.
4247
If the token was consumed or expired, run `node apps/server/src/bin.ts pair`
4348
for a fresh one. Keep using the same tab.

‎.agents/skills/test-t3-mobile/SKILL.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ vp run mobile:install <ios|android> <device-id>
3636

3737
This installs the dev client for the checkout's native fingerprint: from the
3838
device, the local cache, or the shared cache in seconds, building once only
39-
when no machine has that fingerprint yet (5-15 minutes, logged to the file it
40-
names). The device id may be omitted when exactly one device is booted. It
39+
when no machine has that fingerprint yet: in the `Mobile Dev Client` workflow
40+
on Blacksmith when HEAD is pushed and `gh` may dispatch it (the command waits),
41+
otherwise here (5-15 minutes, logged to the file it names). The device id may be omitted when exactly one device is booted. It
4142
prints one JSON line; on failure it names the next command. Mobile verification
4243
includes that step unless the user prohibits it.
4344

‎.github/SECURITY.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Security policy
22

33
Report security vulnerabilities affecting T3 Code or T3 Tools-operated infrastructure to
4-
[security@ping.gg](mailto:security@ping.gg). Please do not disclose them publicly until we have had
4+
[security@t3.codes](mailto:security@t3.codes). Please do not disclose them publicly until we have had
55
a reasonable opportunity to investigate and remediate them.
66

77
See the [full security policy](https://t3.codes/security-policy) for reporting details, scope,

‎.github/actions/ci-changes/action.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ outputs:
55
value: ${{ steps.detect.outputs.typecheck }}
66
build:
77
value: ${{ steps.detect.outputs.build }}
8+
desktop_launch:
9+
value: ${{ steps.detect.outputs.desktop_launch }}
810
test:
911
value: ${{ steps.detect.outputs.test }}
1012
test_packages:

‎.github/actions/setup-ci-deps/action.yml‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ inputs:
1313
description: Arguments for `vp install`, such as filters. Empty installs the whole workspace.
1414
required: false
1515
default: ""
16+
save-pnpm-store:
17+
description: Set to "false" when a concurrent job with the same install args saves the pnpm store
18+
required: false
19+
default: "true"
1620
runs:
1721
using: composite
1822
steps:
@@ -123,10 +127,14 @@ runs:
123127
key: ${{ steps.pnpm_verified.outputs.cache-primary-key }}
124128

125129
# A filtered install that started from the current full store adds nothing
126-
# worth a second copy of it.
130+
# worth a second copy of it. A stacked PR, based on a branch other than the
131+
# default, saves a ~1 GB store only its own reruns can read, so it only restores.
132+
# Concurrent jobs with the same install args reserve one key, so the losers
133+
# compress the store for nothing; all but one pass save-pnpm-store: false.
127134
- name: Save pnpm store
128135
if: >-
129-
inputs.install == 'true' &&
136+
inputs.install == 'true' && inputs.save-pnpm-store == 'true' &&
137+
(github.event_name != 'pull_request' || github.base_ref == github.event.repository.default_branch) &&
130138
steps.pnpm_cache.outputs.cache-hit != 'true' &&
131139
(steps.pnpm_store.outputs.namespace == 'full' ||
132140
steps.pnpm_cache.outputs.cache-matched-key != format('pnpm-full-v1-{0}-{1}-{2}', runner.os, runner.arch, hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', 'package.json')))

‎.github/scripts/check-nightly-release.cjs‎

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,42 +40,67 @@ async function assertReleaseSource({ github, context, releaseChannel }) {
4040

4141
const isNightlyTag = (tag) => /^v.*-nightly\./.test(tag) || tag.startsWith("nightly-v");
4242

43-
async function findLatestNightly({ github, context }) {
44-
const releases = await github.paginate(github.rest.repos.listReleases, {
45-
...context.repo,
46-
per_page: 100,
47-
});
48-
return releases
49-
.filter((release) => !release.draft && release.published_at && isNightlyTag(release.tag_name))
50-
.sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0];
43+
// Only the three fields the gate reads. The REST list also returns every asset,
44+
// which made a full scan tens of megabytes.
45+
const RELEASES_QUERY = `
46+
query ($owner: String!, $repo: String!, $cursor: String) {
47+
repository(owner: $owner, name: $repo) {
48+
releases(first: 100, after: $cursor, orderBy: { field: CREATED_AT, direction: DESC }) {
49+
pageInfo { hasNextPage endCursor }
50+
nodes { tagName isDraft publishedAt }
51+
}
52+
}
53+
}
54+
`;
55+
56+
// Returns the nightly with the newest publishedAt. Release order follows the
57+
// tagged commit date, not publication, so every page is read unless `isSettled`
58+
// says the newest nightly seen so far already decides the caller's answer.
59+
async function findLatestNightly({ github, context, isSettled = () => false }) {
60+
let latest;
61+
let cursor = null;
62+
do {
63+
const { repository } = await github.graphql(RELEASES_QUERY, { ...context.repo, cursor });
64+
const { nodes, pageInfo } = repository.releases;
65+
for (const release of nodes) {
66+
if (release.isDraft || !release.publishedAt || !isNightlyTag(release.tagName)) continue;
67+
if (!latest || Date.parse(release.publishedAt) > Date.parse(latest.publishedAt)) {
68+
latest = release;
69+
}
70+
}
71+
if (latest && isSettled(latest)) return latest;
72+
cursor = pageInfo.hasNextPage ? pageInfo.endCursor : null;
73+
} while (cursor);
74+
return latest;
5175
}
5276

5377
async function shouldReleaseNightly({ github, context, core, now = Date.now() }) {
54-
const lastNightly = await findLatestNightly({ github, context });
78+
const isInGap = (release) => now - Date.parse(release.publishedAt) < MINIMUM_RELEASE_GAP_MS;
79+
// Any nightly inside the gap means the newest one is too, so stop reading.
80+
const lastNightly = await findLatestNightly({ github, context, isSettled: isInGap });
5581

5682
if (!lastNightly) {
5783
core.info("No published nightly found. Proceeding with release.");
5884
return true;
5985
}
6086

61-
if (now - Date.parse(lastNightly.published_at) < MINIMUM_RELEASE_GAP_MS) {
62-
core.info(`Nightly ${lastNightly.tag_name} was published less than six hours ago. Skipping.`);
87+
const tag = lastNightly.tagName;
88+
if (isInGap(lastNightly)) {
89+
core.info(`Nightly ${tag} was published less than six hours ago. Skipping.`);
6390
return false;
6491
}
6592

6693
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
6794
...context.repo,
68-
basehead: `${lastNightly.tag_name}...${context.sha}`,
95+
basehead: `${tag}...${context.sha}`,
6996
per_page: 1,
7097
});
7198
if (comparison.status !== "ahead") {
72-
core.info(
73-
`Candidate commit is ${comparison.status} relative to ${lastNightly.tag_name}. Skipping.`,
74-
);
99+
core.info(`Candidate commit is ${comparison.status} relative to ${tag}. Skipping.`);
75100
return false;
76101
}
77102

78-
core.info(`New commits since ${lastNightly.tag_name}, and the six-hour gap has passed.`);
103+
core.info(`New commits since ${tag}, and the six-hour gap has passed.`);
79104
return true;
80105
}
81106

@@ -85,7 +110,7 @@ async function resolveLatestNightlyCommit({ github, context, core }) {
85110
throw new Error("No published nightly found. Stable releases build the latest nightly commit.");
86111
}
87112

88-
const tag = lastNightly.tag_name;
113+
const tag = lastNightly.tagName;
89114
const { data: commit } = await github.rest.repos.getCommit({ ...context.repo, ref: tag });
90115
const version = /^(?:nightly-)?v(\d+\.\d+\.\d+)-nightly\./.exec(tag)?.[1];
91116
if (!version) {

‎.github/scripts/check-nightly-release.test.cjs‎

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ const {
99
const now = Date.parse("2026-09-05T12:00:00Z");
1010
const hour = 60 * 60 * 1000;
1111
const nightly = (hoursAgo, overrides = {}) => ({
12-
tag_name: "v1.0.1-nightly.20260905.123",
13-
draft: false,
14-
published_at: new Date(now - hoursAgo * hour).toISOString(),
12+
tagName: "v1.0.1-nightly.20260905.123",
13+
isDraft: false,
14+
publishedAt: new Date(now - hoursAgo * hour).toISOString(),
1515
...overrides,
1616
});
1717

@@ -73,26 +73,39 @@ for (const comparisonStatus of ["behind", "diverged"]) {
7373
});
7474
}
7575

76-
function fixture({ releases = [nightly(7)], comparisonStatus = "ahead" } = {}) {
76+
// `pages` holds the GraphQL release pages; `releases` is shorthand for one page.
77+
function fixture({ releases = [nightly(7)], pages = [releases], comparisonStatus = "ahead" } = {}) {
7778
const calls = [];
79+
const cursors = [];
7880
return {
7981
calls,
82+
cursors,
8083
options: {
8184
now,
8285
context: { repo: { owner: "example", repo: "app" }, sha: "new" },
8386
core: { info() {} },
8487
github: {
8588
rest: {
8689
repos: {
87-
listReleases() {},
8890
async compareCommitsWithBasehead(params) {
8991
calls.push(params);
9092
return { data: { status: comparisonStatus } };
9193
},
9294
},
9395
},
94-
async paginate() {
95-
return releases;
96+
async graphql(_query, { owner, repo, cursor }) {
97+
assert.deepEqual({ owner, repo }, { owner: "example", repo: "app" });
98+
cursors.push(cursor);
99+
const index = cursor === null ? 0 : Number(cursor);
100+
const hasNextPage = index + 1 < pages.length;
101+
return {
102+
repository: {
103+
releases: {
104+
nodes: pages[index],
105+
pageInfo: { hasNextPage, endCursor: hasNextPage ? String(index + 1) : null },
106+
},
107+
},
108+
};
96109
},
97110
},
98111
},
@@ -101,28 +114,65 @@ function fixture({ releases = [nightly(7)], comparisonStatus = "ahead" } = {}) {
101114

102115
test("releases the first nightly when no nightly is published", async () => {
103116
const { options } = fixture({
104-
releases: [nightly(0, { tag_name: "v1.0.0" }), nightly(0, { draft: true })],
117+
releases: [nightly(0, { tagName: "v1.0.0" }), nightly(0, { isDraft: true })],
105118
});
106119
assert.equal(await shouldReleaseNightly(options), true);
107120
});
108121

109122
test("uses publication time, not release order or the tagged commit date", async () => {
110123
const { options } = fixture({
111-
releases: [nightly(10), nightly(1), nightly(20, { tag_name: "nightly-v0.9.0" })],
124+
releases: [nightly(10), nightly(1), nightly(20, { tagName: "nightly-v0.9.0" })],
125+
});
126+
assert.equal(await shouldReleaseNightly(options), false);
127+
});
128+
129+
test("the six-hour gap ends exactly six hours after publication", async () => {
130+
const atBoundary = fixture({ releases: [nightly(6)] });
131+
assert.equal(await shouldReleaseNightly(atBoundary.options), true);
132+
const justInside = fixture({ releases: [nightly(6 - 1 / 3_600_000)] });
133+
assert.equal(await shouldReleaseNightly(justInside.options), false);
134+
assert.equal(justInside.calls.length, 0);
135+
});
136+
137+
test("stops reading releases once a nightly inside the gap is seen", async () => {
138+
const { options, cursors } = fixture({ pages: [[nightly(10), nightly(2)], [nightly(30)]] });
139+
assert.equal(await shouldReleaseNightly(options), false);
140+
assert.deepEqual(cursors, [null]);
141+
});
142+
143+
test("reads every page when no nightly seen so far settles the gap", async () => {
144+
const { options, cursors, calls } = fixture({
145+
pages: [
146+
[nightly(10), nightly(0, { tagName: "v1.0.0" })],
147+
[nightly(1, { tagName: "nightly-v0.9.0" })],
148+
],
112149
});
113150
assert.equal(await shouldReleaseNightly(options), false);
151+
assert.deepEqual(cursors, [null, "1"]);
152+
assert.equal(calls.length, 0);
153+
});
154+
155+
test("compares against the newest nightly across pages once the gap has passed", async () => {
156+
const { options, calls } = fixture({
157+
pages: [[nightly(10, { tagName: "v1.0.1-nightly.20260905.100" })], [nightly(8)]],
158+
});
159+
assert.equal(await shouldReleaseNightly(options), true);
160+
assert.deepEqual(
161+
calls.map((call) => call.basehead),
162+
["v1.0.1-nightly.20260905.123...new"],
163+
);
114164
});
115165

116166
test("ignores stable releases and drafts when checking the gap", async () => {
117167
const { options } = fixture({
118-
releases: [nightly(0, { tag_name: "v1.0.0" }), nightly(0, { draft: true }), nightly(7)],
168+
releases: [nightly(0, { tagName: "v1.0.0" }), nightly(0, { isDraft: true }), nightly(7)],
119169
});
120170
assert.equal(await shouldReleaseNightly(options), true);
121171
});
122172

123173
test("fails instead of releasing when GitHub cannot supply release state", async () => {
124174
const { options } = fixture();
125-
options.github.paginate = async () => {
175+
options.github.graphql = async () => {
126176
throw new Error("GitHub unavailable");
127177
};
128178
await assert.rejects(shouldReleaseNightly(options), /GitHub unavailable/);
@@ -137,9 +187,9 @@ for (const status of ["behind", "diverged"]) {
137187

138188
const { resolveLatestNightlyCommit } = require("./check-nightly-release.cjs");
139189

140-
function nightlyCommitFixture({ releases, commitSha = "abc123" }) {
190+
function nightlyCommitFixture({ releases, pages, commitSha = "abc123" }) {
141191
const refs = [];
142-
const { options } = fixture({ releases });
192+
const { options } = fixture({ releases, pages });
143193
options.github.rest.repos.getCommit = async ({ ref }) => {
144194
refs.push(ref);
145195
return { data: { sha: commitSha } };
@@ -150,10 +200,10 @@ function nightlyCommitFixture({ releases, commitSha = "abc123" }) {
150200
test("stable releases resolve the commit of the newest published nightly", async () => {
151201
const { options, refs } = nightlyCommitFixture({
152202
releases: [
153-
nightly(10, { tag_name: "v1.0.1-nightly.20260905.100" }),
154-
nightly(1, { tag_name: "v1.0.1-nightly.20260905.123" }),
155-
nightly(0, { tag_name: "v1.0.0" }),
156-
nightly(0, { draft: true, tag_name: "v1.0.1-nightly.20260905.999" }),
203+
nightly(10, { tagName: "v1.0.1-nightly.20260905.100" }),
204+
nightly(1, { tagName: "v1.0.1-nightly.20260905.123" }),
205+
nightly(0, { tagName: "v1.0.0" }),
206+
nightly(0, { isDraft: true, tagName: "v1.0.1-nightly.20260905.999" }),
157207
],
158208
commitSha: "deadbeef",
159209
});
@@ -167,7 +217,23 @@ test("stable releases resolve the commit of the newest published nightly", async
167217

168218
test("stable releases derive the version from legacy nightly tags", async () => {
169219
const { options } = nightlyCommitFixture({
170-
releases: [nightly(1, { tag_name: "nightly-v0.9.0-nightly.20260905.5" })],
220+
releases: [nightly(1, { tagName: "nightly-v0.9.0-nightly.20260905.5" })],
171221
});
172222
assert.equal((await resolveLatestNightlyCommit(options)).version, "0.9.0");
173223
});
224+
225+
test("stable releases read every page, even after a recent nightly", async () => {
226+
const { options, refs } = nightlyCommitFixture({
227+
pages: [
228+
[nightly(2, { tagName: "v1.0.1-nightly.20260905.100" })],
229+
[nightly(1, { tagName: "v1.0.1-nightly.20260905.123" })],
230+
],
231+
});
232+
assert.equal((await resolveLatestNightlyCommit(options)).tag, "v1.0.1-nightly.20260905.123");
233+
assert.deepEqual(refs, ["v1.0.1-nightly.20260905.123"]);
234+
});
235+
236+
test("stable releases fail when no nightly is published", async () => {
237+
const { options } = nightlyCommitFixture({ releases: [nightly(1, { tagName: "v1.0.0" })] });
238+
await assert.rejects(resolveLatestNightlyCommit(options), /No published nightly found/);
239+
});

0 commit comments

Comments
 (0)