pi-t3-bridge 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/overlay/overlay.json +118 -0
- package/package.json +14 -6
- package/pstack/skills/architect/SKILL.md +1 -1
- package/pstack/skills/arena/SKILL.md +1 -1
- package/pstack/skills/how/SKILL.md +1 -1
- package/pstack/skills/interrogate/SKILL.md +1 -1
- package/pstack/skills/poteto-help/SKILL.md +1 -1
- package/pstack/skills/poteto-mode/SKILL.md +4 -4
- package/pstack/skills/poteto-mode/brief.md +4 -4
- package/pstack/skills/poteto-mode/playbooks/autopilot-full.md +1 -1
- package/pstack/skills/poteto-mode/playbooks/orchestrate.md +1 -1
- package/pstack/skills/reflect/SKILL.md +1 -1
- package/pstack/skills/setup-pstack/SKILL.md +3 -3
- package/pstack/skills/statechart/scripts/run.ts +1 -1
- package/pstack/skills/swarm/SKILL.md +1 -1
- package/pstack/skills/why/SKILL.md +1 -1
- package/src/bridge/accounts.ts +2 -2
- package/src/bridge/ask.ts +2 -2
- package/src/bridge/bash-timeout.ts +1 -1
- package/src/bridge/config.ts +2 -2
- package/src/bridge/plan.ts +1 -1
- package/src/bridge/resume.ts +2 -2
- package/src/bridge/state.ts +2 -2
- package/src/bridge/steer-judge.ts +2 -2
- package/src/bridge/steer-transcript.ts +1 -1
- package/src/bridge/steer.ts +1 -1
- package/src/bridge/t3-json.ts +2 -2
- package/src/bridge/tasks.ts +2 -2
- package/src/bridge/thread.ts +1 -1
- package/src/bridge/transcript.ts +2 -2
- package/src/pstack/brief.ts +1 -1
- package/src/pstack/child.ts +1 -1
- package/src/pstack/models.ts +2 -2
- package/src/pstack/overlay.ts +251 -0
- package/src/pstack/resources.ts +16 -2
- package/src/pstack/results.ts +1 -1
- package/src/pstack/sandbox.ts +1 -1
- package/src/pstack/session.ts +4 -24
- package/tstack/LICENSE +21 -0
- package/tstack/README.md +53 -0
- package/tstack/skills/babysit-pr/SKILL.md +117 -0
- package/tstack/skills/babysit-pr/scripts/pr-wait +556 -0
- package/tstack/skills/council/SKILL.md +56 -0
- package/tstack/skills/file-pr/SKILL.md +50 -0
- package/tstack/skills/full-send/SKILL.md +123 -0
- package/tstack/skills/full-send/agents/openai.yaml +4 -0
- package/tstack/skills/full-stop/SKILL.md +47 -0
- package/tstack/skills/full-stop/agents/openai.yaml +4 -0
- package/tstack/skills/leaving-pr-comments/SKILL.md +44 -0
- package/tstack/skills/leaving-pr-comments/agents/openai.yaml +4 -0
- package/tstack/skills/modernize/SKILL.md +102 -0
- package/tstack/skills/modernize/agents/openai.yaml +4 -0
- package/tstack/skills/sol-loop/SKILL.md +119 -0
- package/tstack/skills/takeover/SKILL.md +42 -0
- package/tstack.ts +21 -0
package/src/pstack/models.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { Type } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { Static } from "@earendil-works/pi-ai";
|
|
5
3
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
6
4
|
import type {
|
|
7
5
|
ExtensionAPI,
|
|
8
6
|
ExtensionToolContext,
|
|
9
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import type { Static } from "typebox";
|
|
10
10
|
import { Value } from "typebox/value";
|
|
11
11
|
|
|
12
12
|
import {
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
symlinkSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
18
|
+
import type { Static } from "@earendil-works/pi-ai";
|
|
19
|
+
import { Value } from "typebox/value";
|
|
20
|
+
|
|
21
|
+
interface Patch {
|
|
22
|
+
readonly find: string;
|
|
23
|
+
readonly replace: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PatchSchema = Type.Object({
|
|
27
|
+
find: Type.String(),
|
|
28
|
+
replace: Type.String(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const OverlayManifestSchema = Type.Object({
|
|
32
|
+
files: Type.Record(Type.String(), Type.Array(PatchSchema)),
|
|
33
|
+
global: Type.Array(PatchSchema),
|
|
34
|
+
shared: Type.Array(
|
|
35
|
+
Type.Object({
|
|
36
|
+
files: Type.Array(Type.String()),
|
|
37
|
+
patches: Type.Array(PatchSchema),
|
|
38
|
+
})
|
|
39
|
+
),
|
|
40
|
+
version: Type.Number(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export type OverlayManifest = Static<typeof OverlayManifestSchema>;
|
|
44
|
+
|
|
45
|
+
const TEXT_SUFFIX = ".md";
|
|
46
|
+
const COPIED_ENTRIES = ["skills", "agents", "models.json"] as const;
|
|
47
|
+
|
|
48
|
+
export const overlayDir = (extensionDir: string): string =>
|
|
49
|
+
path.join(extensionDir, "overlay");
|
|
50
|
+
|
|
51
|
+
export const loadManifest = (dir: string): OverlayManifest => {
|
|
52
|
+
const manifestPath = path.join(dir, "overlay.json");
|
|
53
|
+
const parsed: unknown = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
54
|
+
|
|
55
|
+
if (!Value.Check(OverlayManifestSchema, parsed)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`overlay manifest ${manifestPath} does not match the overlay schema`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return parsed;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const patchesFor = (
|
|
65
|
+
manifest: OverlayManifest,
|
|
66
|
+
relativePath: string
|
|
67
|
+
): readonly Patch[] => {
|
|
68
|
+
const own = manifest.files[relativePath] ?? [];
|
|
69
|
+
|
|
70
|
+
const shared = manifest.shared
|
|
71
|
+
.filter((group) => group.files.includes(relativePath))
|
|
72
|
+
.flatMap((group) => group.patches);
|
|
73
|
+
|
|
74
|
+
return [...shared, ...own];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const applyOverlay = (
|
|
78
|
+
manifest: OverlayManifest,
|
|
79
|
+
relativePath: string,
|
|
80
|
+
text: string
|
|
81
|
+
): string => {
|
|
82
|
+
let result = text;
|
|
83
|
+
|
|
84
|
+
for (const patch of patchesFor(manifest, relativePath)) {
|
|
85
|
+
if (!result.includes(patch.find)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`overlay patch for ${relativePath} no longer matches upstream text: ${JSON.stringify(patch.find.slice(0, 120))}. Update overlay/overlay.json for the new upstream wording.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
result = result.replaceAll(patch.find, patch.replace);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const patch of manifest.global) {
|
|
95
|
+
result = result.replaceAll(patch.find, patch.replace);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const walkFiles = (root: string, base = root): string[] =>
|
|
102
|
+
readdirSync(base, { withFileTypes: true }).flatMap((entry) => {
|
|
103
|
+
const full = path.join(base, entry.name);
|
|
104
|
+
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
return walkFiles(root, full);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return entry.isFile() ? [path.relative(root, full)] : [];
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const overlayCacheRoot = (): string => {
|
|
113
|
+
const stateHome =
|
|
114
|
+
process.env.XDG_STATE_HOME?.trim() ||
|
|
115
|
+
path.join(homedir(), ".local", "state");
|
|
116
|
+
|
|
117
|
+
return path.join(stateHome, "pi-t3", "pstack-overlay");
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const INPUT_ENTRIES = [
|
|
121
|
+
...COPIED_ENTRIES,
|
|
122
|
+
"hooks/session-start-context.md",
|
|
123
|
+
] as const;
|
|
124
|
+
|
|
125
|
+
const fingerprint = (
|
|
126
|
+
upstreamRoot: string,
|
|
127
|
+
manifestText: string,
|
|
128
|
+
extensionModules: string
|
|
129
|
+
): string => {
|
|
130
|
+
const hash = createHash("sha256")
|
|
131
|
+
.update(manifestText)
|
|
132
|
+
.update(extensionModules);
|
|
133
|
+
|
|
134
|
+
for (const entry of INPUT_ENTRIES) {
|
|
135
|
+
const full = path.join(upstreamRoot, entry);
|
|
136
|
+
|
|
137
|
+
if (!existsSync(full)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const files = statSync(full).isDirectory() ? walkFiles(full) : [entry];
|
|
142
|
+
|
|
143
|
+
for (const relative of files.toSorted()) {
|
|
144
|
+
const filePath = statSync(full).isDirectory()
|
|
145
|
+
? path.join(full, relative)
|
|
146
|
+
: full;
|
|
147
|
+
|
|
148
|
+
const name = statSync(full).isDirectory()
|
|
149
|
+
? path.join(entry, relative)
|
|
150
|
+
: entry;
|
|
151
|
+
|
|
152
|
+
hash.update(name).update(readFileSync(filePath));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return hash.digest("hex").slice(0, 16);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// The pstack the agent reads: upstream copied out of pstack/, with the
|
|
160
|
+
// overlay in overlay/ applied. A copy keyed by the hash of both inputs, so
|
|
161
|
+
// an upstream sync or an overlay edit builds a fresh copy and a stale one
|
|
162
|
+
// is never served. Nothing here writes into pstack/.
|
|
163
|
+
export const materializePstack = (
|
|
164
|
+
upstreamRoot: string,
|
|
165
|
+
dir: string,
|
|
166
|
+
cacheRoot: string = overlayCacheRoot()
|
|
167
|
+
): string => {
|
|
168
|
+
if (!existsSync(path.join(upstreamRoot, "skills"))) {
|
|
169
|
+
return upstreamRoot;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const manifestText = readFileSync(path.join(dir, "overlay.json"), "utf-8");
|
|
173
|
+
const manifest = loadManifest(dir);
|
|
174
|
+
|
|
175
|
+
const extensionModules = path.resolve(
|
|
176
|
+
path.dirname(upstreamRoot),
|
|
177
|
+
"node_modules"
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const target = path.join(
|
|
181
|
+
cacheRoot,
|
|
182
|
+
fingerprint(upstreamRoot, manifestText, extensionModules)
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
existsSync(path.join(target, ".complete")) &&
|
|
187
|
+
(!existsSync(extensionModules) ||
|
|
188
|
+
existsSync(path.join(target, "node_modules")))
|
|
189
|
+
) {
|
|
190
|
+
return target;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
mkdirSync(cacheRoot, { recursive: true });
|
|
194
|
+
rmSync(target, { force: true, recursive: true });
|
|
195
|
+
|
|
196
|
+
const staging = `${target}.${process.pid}.tmp`;
|
|
197
|
+
|
|
198
|
+
rmSync(staging, { force: true, recursive: true });
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
for (const entry of COPIED_ENTRIES) {
|
|
202
|
+
const from = path.join(upstreamRoot, entry);
|
|
203
|
+
|
|
204
|
+
if (existsSync(from)) {
|
|
205
|
+
cpSync(from, path.join(staging, entry), { recursive: true });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const hookFrom = path.join(
|
|
210
|
+
upstreamRoot,
|
|
211
|
+
"hooks",
|
|
212
|
+
"session-start-context.md"
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (existsSync(hookFrom)) {
|
|
216
|
+
mkdirSync(path.join(staging, "hooks"), { recursive: true });
|
|
217
|
+
cpSync(hookFrom, path.join(staging, "hooks", "session-start-context.md"));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Scripts under skills/ import packages such as commander. Inside the
|
|
221
|
+
// extension they resolve them from the extension's node_modules, so
|
|
222
|
+
// the copy links that directory in instead of copying it.
|
|
223
|
+
if (
|
|
224
|
+
existsSync(extensionModules) &&
|
|
225
|
+
!existsSync(path.join(staging, "node_modules"))
|
|
226
|
+
) {
|
|
227
|
+
symlinkSync(extensionModules, path.join(staging, "node_modules"), "dir");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const relative of walkFiles(staging)) {
|
|
231
|
+
if (!relative.endsWith(TEXT_SUFFIX)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const filePath = path.join(staging, relative);
|
|
236
|
+
|
|
237
|
+
writeFileSync(
|
|
238
|
+
filePath,
|
|
239
|
+
applyOverlay(manifest, relative, readFileSync(filePath, "utf-8"))
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
writeFileSync(path.join(staging, ".complete"), "");
|
|
244
|
+
renameSync(staging, target);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
rmSync(staging, { force: true, recursive: true });
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return target;
|
|
251
|
+
};
|
package/src/pstack/resources.ts
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { materializePstack, overlayDir } from "./overlay.ts";
|
|
5
|
+
|
|
4
6
|
export interface PstackPaths {
|
|
5
7
|
readonly extensionDir: string;
|
|
6
8
|
readonly pstackRoot: string;
|
|
7
9
|
readonly skillsDir: string;
|
|
10
|
+
readonly upstreamRoot: string;
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
let cached: PstackPaths | undefined;
|
|
14
|
+
|
|
10
15
|
export const pstackPaths = (): PstackPaths => {
|
|
16
|
+
if (cached) {
|
|
17
|
+
return cached;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
const extensionDir = path.resolve(import.meta.dirname, "../..");
|
|
12
|
-
const
|
|
21
|
+
const upstreamRoot = path.join(extensionDir, "pstack");
|
|
22
|
+
|
|
23
|
+
const pstackRoot = materializePstack(upstreamRoot, overlayDir(extensionDir));
|
|
13
24
|
|
|
14
|
-
|
|
25
|
+
cached = {
|
|
15
26
|
extensionDir,
|
|
16
27
|
pstackRoot,
|
|
17
28
|
skillsDir: path.join(pstackRoot, "skills"),
|
|
29
|
+
upstreamRoot,
|
|
18
30
|
};
|
|
31
|
+
|
|
32
|
+
return cached;
|
|
19
33
|
};
|
|
20
34
|
|
|
21
35
|
export const policyPath = (): string =>
|
package/src/pstack/results.ts
CHANGED
package/src/pstack/sandbox.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
|
|
7
|
-
import { Type } from "
|
|
7
|
+
import { Type } from "typebox";
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
|
|
10
10
|
import { readConfig } from "../bridge/config.ts";
|
package/src/pstack/session.ts
CHANGED
|
@@ -57,22 +57,8 @@ export const sessionHookEnabled = (): boolean => {
|
|
|
57
57
|
return !SESSION_HOOK_OFF.test(readFileSync(sheetPath, "utf-8"));
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// model sheet and no shell output budget cuts a SKILL.md.
|
|
63
|
-
const PI_REWRITES: readonly (readonly [string, string])[] = [
|
|
64
|
-
["You have t3-pstack,", "You have pi-t3-bridge,"],
|
|
65
|
-
["t3-pstack:poteto-mode", "/skill:poteto-mode"],
|
|
66
|
-
[
|
|
67
|
-
" On Codex, open its SKILL.md whole, in a shell call of its own with `max_output_tokens: 10000`, since a smaller budget cuts out its middle.",
|
|
68
|
-
" Read its SKILL.md in full before you act.",
|
|
69
|
-
],
|
|
70
|
-
[
|
|
71
|
-
"This runs on T3: before the first subagent or panel in a session, read the model sheet, `~/.config/t3-pstack/models.md` (`$XDG_CONFIG_HOME/t3-pstack/models.md` when that is set), or `t3-pstack-models.md` in this host's config directory when that file is missing. This chat runs the playbook itself. A step it says to delegate goes through `delegate_task` to that role's sheet entry,",
|
|
72
|
-
"This chat runs the playbook itself. A step it says to delegate goes through `delegate_task` with a target from `pstack_model`,",
|
|
73
|
-
],
|
|
74
|
-
];
|
|
75
|
-
|
|
60
|
+
// paths.pstackRoot already has the overlay applied, so the hook text on
|
|
61
|
+
// disk is the Pi text. The overlay owns every rewrite of upstream wording.
|
|
76
62
|
export const sessionGuidance = (paths: PstackPaths): string => {
|
|
77
63
|
const contextPath = path.join(
|
|
78
64
|
paths.pstackRoot,
|
|
@@ -80,18 +66,12 @@ export const sessionGuidance = (paths: PstackPaths): string => {
|
|
|
80
66
|
"session-start-context.md"
|
|
81
67
|
);
|
|
82
68
|
|
|
83
|
-
const
|
|
69
|
+
const guidance = existsSync(contextPath)
|
|
84
70
|
? readFileSync(contextPath, "utf-8").trim()
|
|
85
71
|
: "You have pi-t3-bridge, pstack for T3 Code.";
|
|
86
72
|
|
|
87
|
-
let piGuidance = upstreamGuidance;
|
|
88
|
-
|
|
89
|
-
for (const [from, to] of PI_REWRITES) {
|
|
90
|
-
piGuidance = piGuidance.replaceAll(from, to);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
73
|
return [
|
|
94
|
-
|
|
74
|
+
guidance,
|
|
95
75
|
"",
|
|
96
76
|
"Pi notes:",
|
|
97
77
|
`- The pstack root is ${paths.pstackRoot}. Read skill and agent files there when a skill names a plugin root.`,
|
package/tstack/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Theo Browne
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/tstack/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# tstack
|
|
2
|
+
|
|
3
|
+
The agent skills I use every day, cleaned up so anyone can use them. They work
|
|
4
|
+
in Claude Code and Codex. Some use [T3 Code](https://github.com/pingdotgg/t3code)
|
|
5
|
+
features when they are available and fall back to native tools when they are
|
|
6
|
+
not.
|
|
7
|
+
|
|
8
|
+
## Skills
|
|
9
|
+
|
|
10
|
+
| Skill | Say | What it does |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `full-send` | "full send" | Takes a change to a merged PR with nobody watching: proves it, reviews it, files it, babysits it, merges it. |
|
|
13
|
+
| `full-stop` | "full stop" | Same as `full-send`, but stops when the PR is ready for you to merge. |
|
|
14
|
+
| `council` | "council" | Gets review from three models from different providers, and loops until they agree. |
|
|
15
|
+
| `sol-loop` | "sol loop" | Reviews a change with GPT Sol in a loop until both agents agree. |
|
|
16
|
+
| `file-pr` | "file a PR" | Files a short PR that leads with the problem, not an inventory of changes. |
|
|
17
|
+
| `takeover` | "take over" | Finishes someone else's PR, branch, or agent thread, and keeps their credit. |
|
|
18
|
+
| `babysit-pr` | "babysit" | Watches a PR through CI and review bots, and fixes the real findings. |
|
|
19
|
+
| `modernize` | "modernize" | Rebases an old PR and decides if it should still exist. |
|
|
20
|
+
| `leaving-pr-comments` | (automatic) | Formats comments that an agent posts on your behalf. |
|
|
21
|
+
|
|
22
|
+
Some skills call others. Install the ones they need too:
|
|
23
|
+
|
|
24
|
+
- `full-send` uses `sol-loop`, `file-pr`, and `babysit-pr`.
|
|
25
|
+
- `full-stop` uses `full-send`.
|
|
26
|
+
- `takeover` uses `modernize`, `file-pr`, `babysit-pr`, and `leaving-pr-comments`.
|
|
27
|
+
- `file-pr`, `babysit-pr`, and `modernize` use `leaving-pr-comments`.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/t3dotgg/tstack
|
|
33
|
+
cp -R tstack/skills/* ~/.claude/skills/ # Claude Code
|
|
34
|
+
cp -R tstack/skills/* ~/.codex/skills/ # Codex
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- An authenticated [GitHub CLI](https://cli.github.com) (`gh`).
|
|
40
|
+
- `python3` for `babysit-pr`'s `pr-wait` script.
|
|
41
|
+
- `council` and `sol-loop` need subagents on other providers' models. T3 Code
|
|
42
|
+
does this with `delegate_task`. Otherwise you need a harness that can run
|
|
43
|
+
those models.
|
|
44
|
+
|
|
45
|
+
## Make them yours
|
|
46
|
+
|
|
47
|
+
These are starting points. Edit them. Put personal settings, like which models
|
|
48
|
+
sit on your council, in your `CLAUDE.md` or `AGENTS.md`, so you can update the
|
|
49
|
+
skills without losing them.
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: babysit-pr
|
|
3
|
+
description: Monitor a pull request through review and CI. Use when the user asks to monitor, watch, or babysit a PR.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Babysit PR
|
|
7
|
+
|
|
8
|
+
Many repos have AI review bots. They're helpful, even if they are not always
|
|
9
|
+
right.
|
|
10
|
+
|
|
11
|
+
## Waiting
|
|
12
|
+
|
|
13
|
+
Never write your own watcher, polling loop, or `sleep` loop. Use one watcher
|
|
14
|
+
per PR: `watch_pull_request` in T3 Code, `pr-wait` everywhere else. Never run
|
|
15
|
+
both on the same PR. Every watcher on a GitHub account shares one rate limit.
|
|
16
|
+
|
|
17
|
+
### In T3 Code: `watch_pull_request`
|
|
18
|
+
|
|
19
|
+
Handle the existing checks and comments first. Then call
|
|
20
|
+
`watch_pull_request` and end your turn. T3 Code wakes you with a message when
|
|
21
|
+
a check fails, the required checks pass, someone comments or reviews, or the
|
|
22
|
+
branch conflicts.
|
|
23
|
+
|
|
24
|
+
- **Only the thread that owns the PR watches it.** If you are a delegated
|
|
25
|
+
child task (a reviewer, a Sol round, any `delegate_task` child), never call
|
|
26
|
+
`watch_pull_request`. The parent owns the PR. Do not watch PRs you are not
|
|
27
|
+
working on.
|
|
28
|
+
- **A wake is news, not a merge decision.** Check the merge gate yourself.
|
|
29
|
+
- **Unwatch when you hand off.** A watched thread stays in Working and out of
|
|
30
|
+
the user's inbox. When you stop (ready, blocked, or asking the user a question),
|
|
31
|
+
call `unwatch_pull_request` first. A merge or close ends the watch on its
|
|
32
|
+
own.
|
|
33
|
+
- **"Stopped watching, could not read it"** usually means GitHub's rate limit
|
|
34
|
+
is used up. Run `gh api rate_limit --jq .resources.graphql` before you
|
|
35
|
+
watch again. If `remaining` is near 0, wait for `reset` with one background
|
|
36
|
+
command, then watch again. Never watch again in a tight loop.
|
|
37
|
+
|
|
38
|
+
### Everywhere else: `pr-wait`
|
|
39
|
+
|
|
40
|
+
Wait with `scripts/pr-wait` from this skill's directory (use its absolute
|
|
41
|
+
path).
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pr-wait <pr> [--repo owner/name] [--until ready|green|any-change]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
It prints one line per real event. It exits 0 when the `--until` condition is
|
|
48
|
+
met or the PR merges, 1 when it needs you (a required check failed, conflicts,
|
|
49
|
+
unresolved threads, PR closed), and 2 on bad input.
|
|
50
|
+
|
|
51
|
+
- `ready` (default): required checks green, Greptile or Macroscope reviewed the
|
|
52
|
+
head, and no unresolved threads. Use this to babysit.
|
|
53
|
+
- `green`: required checks only. Use it when the repo has no Greptile or
|
|
54
|
+
Macroscope.
|
|
55
|
+
- `any-change`: exit on the first event.
|
|
56
|
+
|
|
57
|
+
In Claude Code, run it with the Monitor tool (`timeout_ms: 3600000`), so each
|
|
58
|
+
line wakes you. Without a monitor, run it as one long command. It ends on its
|
|
59
|
+
own.
|
|
60
|
+
|
|
61
|
+
Start it again after you push, after you handle an exit 1, and after a resume.
|
|
62
|
+
It remembers what it already reported, so nothing that arrived in between is
|
|
63
|
+
lost. If it says another pid already watches the PR, do not start a second one.
|
|
64
|
+
Kill the old one only if its events do not reach you.
|
|
65
|
+
|
|
66
|
+
With either watcher: when an event needs no action, end your turn without a
|
|
67
|
+
message. Never post or say "still waiting".
|
|
68
|
+
|
|
69
|
+
## GitHub rate limits
|
|
70
|
+
|
|
71
|
+
`gh pr create`, `gh pr view`, and `gh pr checks` use GraphQL. When GraphQL is
|
|
72
|
+
used up, the REST limit is separate and usually still has room. Use
|
|
73
|
+
`gh api repos/<owner>/<repo>/pulls/<n>` to read a PR, and
|
|
74
|
+
`gh api repos/<owner>/<repo>/commits/<sha>/check-runs` for checks.
|
|
75
|
+
|
|
76
|
+
## Reviews
|
|
77
|
+
|
|
78
|
+
Only act on checks and comments newer than the latest push. Verify every bot
|
|
79
|
+
finding against the source before changing code. Fix real findings and CI
|
|
80
|
+
failures, distinguish repository failures from infrastructure flakes, and reply
|
|
81
|
+
with a written reason when dismissing false positives.
|
|
82
|
+
|
|
83
|
+
If the repo has `.macroscope/check-run-agents/*.md`, read those rules before you
|
|
84
|
+
reply to a Macroscope finding. Its findings cite them.
|
|
85
|
+
|
|
86
|
+
CodeRabbit is advisory. Treat its findings like any other finding, but never
|
|
87
|
+
wait for it and never hold a merge for it.
|
|
88
|
+
|
|
89
|
+
If a review bot leaves feedback you believe is not worth addressing, reply with
|
|
90
|
+
a written reason and resolve the comment. Use the `leaving-pr-comments` skill
|
|
91
|
+
for every comment posted on the user's behalf.
|
|
92
|
+
|
|
93
|
+
Screenshots and videos help as well, when you have a way to upload them.
|
|
94
|
+
|
|
95
|
+
Keep the PR at the size the user's goal needs. Review loops tend to grow a
|
|
96
|
+
PR one cheap-looking fix at a time. Fix real bugs in the intended case.
|
|
97
|
+
Decline the rest with a written reason, and list them as follow-ups:
|
|
98
|
+
unneeded tests, guards for edge cases that do not matter, extra integration
|
|
99
|
+
or compatibility, and small performance costs. Prefer the fix that deletes or
|
|
100
|
+
narrows code.
|
|
101
|
+
|
|
102
|
+
## Base branch and merging
|
|
103
|
+
|
|
104
|
+
Keep an eye on changes to `main` and rebase when needed. `pr-wait` reports when
|
|
105
|
+
new base commits touch this PR's files, and when the PR becomes BEHIND or DIRTY.
|
|
106
|
+
`watch_pull_request` reports conflicts only, so check for BEHIND yourself
|
|
107
|
+
before you call the PR ready.
|
|
108
|
+
If an overlapping PR makes this one obsolete, stop monitoring, report it to the
|
|
109
|
+
user, and ask before closing the PR unless closure was explicitly authorized.
|
|
110
|
+
|
|
111
|
+
If nothing has changed, stay quiet rather than posting filler comments. Stop
|
|
112
|
+
when `pr-wait` exits 0, or in T3 Code when the merge gate passes on the latest
|
|
113
|
+
head. Merge only when the user explicitly requested it;
|
|
114
|
+
otherwise report that the PR is ready. Right before a merge, run `git fetch` and
|
|
115
|
+
check the PR again. Never merge when it is BEHIND or DIRTY, or when base commits
|
|
116
|
+
that landed after the last green run touch this PR's files. Rebase, push, and
|
|
117
|
+
wait for green again.
|