Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2de4a5d6d | |||
| 0f4c2c274a | |||
| 8d654d69a4 |
@@ -6,16 +6,11 @@ The user's rule, from the Deno days and restated for Node (2026-10-12): "No
|
||||
JS, not even JSX/TSX, only TS." Source, scripts, tests, and configs are all
|
||||
plain `.ts`. Build output in a gitignored folder is the only JS.
|
||||
|
||||
`tsconfig.json`: no `allowJs`, no `jsx`, and exactly this strictness set
|
||||
(user decision, 2026-10-18): `strict` (written out), `noUncheckedIndexedAccess`,
|
||||
`noPropertyAccessFromIndexSignature`, `noImplicitReturns`, `noUnusedLocals`,
|
||||
`noUnusedParameters`, `noImplicitOverride`, `noFallthroughCasesInSwitch`,
|
||||
`erasableSyntaxOnly`, plus the mechanical `verbatimModuleSyntax`,
|
||||
`allowImportingTsExtensions`, and `noEmit` (packages also keep `declaration`
|
||||
and `isolatedDeclarations`). `exactOptionalPropertyTypes` is off everywhere:
|
||||
write `{ key: maybeUndefined }` rather than leaving the key out to please it.
|
||||
If a tool or framework seems to need a JS or TSX file, stop and flag it
|
||||
instead of adding one.
|
||||
`tsconfig.json`: no `allowJs`, no `jsx`, and the template's strict flags
|
||||
(`verbatimModuleSyntax`, `erasableSyntaxOnly`, `allowImportingTsExtensions`,
|
||||
`isolatedDeclarations` in packages, `exactOptionalPropertyTypes`,
|
||||
`noUncheckedIndexedAccess`). If a tool or framework seems to need a JS or TSX
|
||||
file, stop and flag it instead of adding one.
|
||||
|
||||
## No asset imports (2026-10-14, widened 2026-10-16 and 2026-10-17)
|
||||
|
||||
|
||||
@@ -15,12 +15,9 @@ explicit permission.
|
||||
The action's TypeScript runs directly on the job image's Node.js (24.2 or
|
||||
newer), which strips the types itself: there is no build step and no runtime
|
||||
dependency. Keep the sources to erasable syntax and `.ts` imports, which
|
||||
`tsconfig.json` enforces. Its strictness set is fixed and leaves
|
||||
`exactOptionalPropertyTypes` off
|
||||
([.agents/memory/ts-only-files-and-imports.md](.agents/memory/ts-only-files-and-imports.md)).
|
||||
Checks use Vite+: `vp check` formats, lints, and type-checks, and `vp test`
|
||||
runs Vitest. Unit tests sit next to the code as `src/*.test.ts`;
|
||||
repository-level tests live in `tests/`.
|
||||
`tsconfig.json` enforces. Checks use Vite+: `vp check` formats, lints, and
|
||||
type-checks, and `vp test` runs Vitest. Unit tests sit next to the code as
|
||||
`src/*.test.ts`; repository-level tests live in `tests/`.
|
||||
|
||||
A `#` alias in `package.json` `imports` may point anywhere and owns its
|
||||
target: an alias to a folder's `index.ts` owns that folder and everything
|
||||
|
||||
+3
-3
@@ -75,11 +75,11 @@ async function readCodexAuth(path: string): Promise<string> {
|
||||
} catch {
|
||||
throw new Error("invalid Codex login");
|
||||
}
|
||||
const tokens = isObject(value) ? value["tokens"] : undefined;
|
||||
const tokens = isObject(value) ? value.tokens : undefined;
|
||||
if (
|
||||
!isObject(value) ||
|
||||
typeof value["auth_mode"] !== "string" ||
|
||||
value["auth_mode"] === "" ||
|
||||
typeof value.auth_mode !== "string" ||
|
||||
value.auth_mode === "" ||
|
||||
!isObject(tokens) ||
|
||||
!["access_token", "refresh_token", "id_token", "account_id"].every(
|
||||
(name) => typeof tokens[name] === "string" && tokens[name] !== "",
|
||||
|
||||
+14
-14
@@ -70,9 +70,9 @@ async function readOutcome(path: string): Promise<RuntimeOutcome> {
|
||||
throw new Error("invalid agent outcome");
|
||||
}
|
||||
const outcome = value;
|
||||
if (outcome["ok"] === false) {
|
||||
if (outcome.ok === false) {
|
||||
// Only known labels reach the comment, never raw agent output.
|
||||
const failures: unknown[] = Array.isArray(outcome["failures"]) ? outcome["failures"] : [];
|
||||
const failures: unknown[] = Array.isArray(outcome.failures) ? outcome.failures : [];
|
||||
return {
|
||||
ok: false,
|
||||
failures: failures.flatMap((entry: unknown) => {
|
||||
@@ -84,26 +84,26 @@ async function readOutcome(path: string): Promise<RuntimeOutcome> {
|
||||
};
|
||||
}
|
||||
if (
|
||||
outcome["ok"] !== true ||
|
||||
typeof outcome["body"] !== "string" ||
|
||||
typeof outcome["reviewPath"] !== "string" ||
|
||||
typeof outcome["anchorsPath"] !== "string" ||
|
||||
!isObject(outcome["profile"])
|
||||
outcome.ok !== true ||
|
||||
typeof outcome.body !== "string" ||
|
||||
typeof outcome.reviewPath !== "string" ||
|
||||
typeof outcome.anchorsPath !== "string" ||
|
||||
!isObject(outcome.profile)
|
||||
) {
|
||||
throw new Error("invalid agent outcome");
|
||||
}
|
||||
const profile = outcome["profile"];
|
||||
if (!isProfileType(profile["type"]) || typeof profile["model"] !== "string") {
|
||||
const profile = outcome.profile;
|
||||
if (!isProfileType(profile.type) || typeof profile.model !== "string") {
|
||||
throw new Error("invalid agent outcome");
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
body: outcome["body"],
|
||||
reviewPath: outcome["reviewPath"],
|
||||
anchorsPath: outcome["anchorsPath"],
|
||||
body: outcome.body,
|
||||
reviewPath: outcome.reviewPath,
|
||||
anchorsPath: outcome.anchorsPath,
|
||||
profile: {
|
||||
type: profile["type"],
|
||||
model: profile["model"],
|
||||
type: profile.type,
|
||||
model: profile.model,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+2
-2
@@ -110,8 +110,8 @@ export function agentEnvironment(
|
||||
token?: string,
|
||||
): Record<string, string> {
|
||||
const result = sanitizedEnvironment(environment);
|
||||
result["HOME"] = profile.home;
|
||||
if (profile.type === "codex") result["CODEX_HOME"] = `${profile.home}/.codex`;
|
||||
result.HOME = profile.home;
|
||||
if (profile.type === "codex") result.CODEX_HOME = `${profile.home}/.codex`;
|
||||
if (token !== undefined && token !== "") {
|
||||
const tokenName = {
|
||||
claude: "CLAUDE_CODE_OAUTH_TOKEN",
|
||||
|
||||
+13
-12
@@ -86,7 +86,7 @@ async function gitea(path: string, body?: unknown): Promise<Response> {
|
||||
Authorization: `token ${AUTHOR_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
body: body === undefined ? null : JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`${path}: ${response.status} ${await response.text()}`);
|
||||
@@ -105,11 +105,11 @@ async function runRoot(command: string, args: string[]): Promise<void> {
|
||||
|
||||
async function configureGitAuthor(): Promise<void> {
|
||||
const user: unknown = await (await gitea("user")).json();
|
||||
if (!isObject(user) || typeof user["login"] !== "string" || typeof user["email"] !== "string") {
|
||||
if (!isObject(user) || typeof user.login !== "string" || typeof user.email !== "string") {
|
||||
throw new Error("user: unexpected Gitea response");
|
||||
}
|
||||
await runRoot("git", ["config", "user.name", user["login"]]);
|
||||
await runRoot("git", ["config", "user.email", user["email"]]);
|
||||
await runRoot("git", ["config", "user.name", user.login]);
|
||||
await runRoot("git", ["config", "user.email", user.email]);
|
||||
}
|
||||
|
||||
async function readProfileToken(profile: RuntimeProfile): Promise<string | undefined> {
|
||||
@@ -155,7 +155,7 @@ async function prepareProfile(profile: RuntimeProfile): Promise<void> {
|
||||
// One issue comment as the prompt quotes it.
|
||||
function formatComment(comment: unknown): string {
|
||||
const { user, created_at: createdAt, body } = isObject(comment) ? comment : {};
|
||||
const login = isObject(user) ? user["login"] : undefined;
|
||||
const login = isObject(user) ? user.login : undefined;
|
||||
return `## ${String(login)} at ${String(createdAt)}\n\n${String(body)}\n`;
|
||||
}
|
||||
|
||||
@@ -231,23 +231,24 @@ async function runClaude(profile: RuntimeProfile, token: string, prompt: string)
|
||||
if (line !== "") {
|
||||
const event: unknown = JSON.parse(line);
|
||||
if (isObject(event)) {
|
||||
const content = isObject(event["message"]) ? event["message"]["content"] : undefined;
|
||||
const content = isObject(event.message) ? event.message.content : undefined;
|
||||
for (const part of Array.isArray(content) ? content : []) {
|
||||
if (isObject(part)) {
|
||||
const text = part["thinking"] ?? part["text"] ?? part["name"];
|
||||
const text = part.thinking ?? part.text ?? part.name;
|
||||
if (typeof text === "string" && text !== "") {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event["type"] === "result") {
|
||||
if (event.type === "result") {
|
||||
const { is_error: isError, result: answer, subtype, api_error_status: apiStatus } = event;
|
||||
result = {
|
||||
is_error: typeof isError === "boolean" ? isError : undefined,
|
||||
result: typeof answer === "string" ? answer : undefined,
|
||||
...(typeof isError === "boolean" && { is_error: isError }),
|
||||
...(typeof answer === "string" && { result: answer }),
|
||||
subtype: String(subtype),
|
||||
api_error_status:
|
||||
typeof apiStatus === "number" || apiStatus === null ? apiStatus : undefined,
|
||||
...((typeof apiStatus === "number" || apiStatus === null) && {
|
||||
api_error_status: apiStatus,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -101,11 +101,11 @@ export async function prepareRuntime(
|
||||
function isRuntimeManifest(value: unknown): value is RuntimeManifest {
|
||||
return (
|
||||
isMapping(value) &&
|
||||
typeof value["runtimeDir"] === "string" &&
|
||||
typeof value["manifestPath"] === "string" &&
|
||||
typeof value["reviewerFile"] === "string" &&
|
||||
typeof value["outcomeFile"] === "string" &&
|
||||
Array.isArray(value["profiles"])
|
||||
typeof value.runtimeDir === "string" &&
|
||||
typeof value.manifestPath === "string" &&
|
||||
typeof value.reviewerFile === "string" &&
|
||||
typeof value.outcomeFile === "string" &&
|
||||
Array.isArray(value.profiles)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const PHASES: [phase: string, firstVariable: string][] = [
|
||||
|
||||
test.each(PHASES)("Node loads the %s phase", async (phase, firstVariable) => {
|
||||
const path = fileURLToPath(new URL(`../src/${phase}.ts`, import.meta.url));
|
||||
const result = await runCommand(process.execPath, [path], { PATH: process.env["PATH"] ?? "" });
|
||||
const result = await runCommand(process.execPath, [path], { PATH: process.env.PATH ?? "" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.stderr).toContain(`${firstVariable} is not set`);
|
||||
});
|
||||
|
||||
+21
-21
@@ -48,11 +48,11 @@ test("all committed YAML parses", async () => {
|
||||
|
||||
test("the action exposes only the master key and scopes it to preparation", async () => {
|
||||
const action = await yaml("action.yml");
|
||||
expect(Object.keys(mapping(action["inputs"], "inputs"))).toStrictEqual(["master-key"]);
|
||||
const steps = sequence(mapping(action["runs"], "runs")["steps"], "runs.steps");
|
||||
expect(Object.keys(mapping(action.inputs, "inputs"))).toStrictEqual(["master-key"]);
|
||||
const steps = sequence(mapping(action.runs, "runs").steps, "runs.steps");
|
||||
for (const step of steps) {
|
||||
if (typeof step["run"] === "string") {
|
||||
expect(step["run"].includes("\n"), String(step["name"])).toBe(false);
|
||||
if (typeof step.run === "string") {
|
||||
expect(step.run.includes("\n"), String(step.name)).toBe(false);
|
||||
}
|
||||
}
|
||||
const checkout = findStep(steps, "uses", "actions/checkout@v4");
|
||||
@@ -60,16 +60,16 @@ test("the action exposes only the master key and scopes it to preparation", asyn
|
||||
const agent = findStep(steps, "id", "agent");
|
||||
const post = findStep(steps, "id", "post");
|
||||
findStep(steps, "id", "source");
|
||||
expect(mapping(checkout["with"], "checkout.with")["token"]).toBe("${{ gitea.token }}");
|
||||
expect(mapping(prepare["env"], "prepare.env")["BOT_AGENTS_MASTER_KEY"]).toBe(
|
||||
expect(mapping(checkout.with, "checkout.with").token).toBe("${{ gitea.token }}");
|
||||
expect(mapping(prepare.env, "prepare.env").BOT_AGENTS_MASTER_KEY).toBe(
|
||||
"${{ inputs.master-key }}",
|
||||
);
|
||||
expect(JSON.stringify(agent).includes("BOT_AGENTS_MASTER_KEY")).toBe(false);
|
||||
expect(JSON.stringify(post).includes("BOT_AGENTS_MASTER_KEY")).toBe(false);
|
||||
expect(mapping(agent["env"], "agent.env")["GITEA_TOKEN"]).toBe("${{ gitea.token }}");
|
||||
assert(String(post["if"]).includes("always()"));
|
||||
expect(mapping(agent.env, "agent.env").GITEA_TOKEN).toBe("${{ gitea.token }}");
|
||||
assert(String(post.if).includes("always()"));
|
||||
for (const step of [prepare, agent, post]) {
|
||||
assert(String(step["run"]).includes("steps.source.outputs.path"));
|
||||
assert(String(step.run).includes("steps.source.outputs.path"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -118,17 +118,17 @@ test("module-relative runtime assets exist", async () => {
|
||||
|
||||
test("the repository uses its trusted main action", async () => {
|
||||
const workflow = await yaml(".gitea/workflows/agents.yml");
|
||||
const job = mapping(mapping(workflow["jobs"], "jobs")["agent"], "jobs.agent");
|
||||
const permissions = mapping(job["permissions"], "jobs.agent.permissions");
|
||||
const job = mapping(mapping(workflow.jobs, "jobs").agent, "jobs.agent");
|
||||
const permissions = mapping(job.permissions, "jobs.agent.permissions");
|
||||
expect(permissions).toStrictEqual({
|
||||
code: "write",
|
||||
issues: "write",
|
||||
"pull-requests": "write",
|
||||
});
|
||||
const steps = sequence(job["steps"], "jobs.agent.steps");
|
||||
expect(steps.findIndex((step) => step["uses"] === "./")).toStrictEqual(-1);
|
||||
const steps = sequence(job.steps, "jobs.agent.steps");
|
||||
expect(steps.findIndex((step) => step.uses === "./")).toStrictEqual(-1);
|
||||
const trusted = findStep(steps, "uses", "https://git.capsulizers.com/commons/bot-agents@main");
|
||||
expect(mapping(trusted["with"], "trusted.with")["master-key"]).toBe(
|
||||
expect(mapping(trusted.with, "trusted.with")["master-key"]).toBe(
|
||||
"${{ secrets.BOT_AGENTS_MASTER_KEY }}",
|
||||
);
|
||||
expect(JSON.stringify(workflow).includes("gitea.actor != 'bot'")).toBe(true);
|
||||
@@ -136,9 +136,9 @@ test("the repository uses its trusted main action", async () => {
|
||||
|
||||
test("CI checks every phase and the root isolation suite", async () => {
|
||||
const workflow = await yaml(".gitea/workflows/check.yml");
|
||||
const job = mapping(mapping(workflow["jobs"], "jobs")["check"], "jobs.check");
|
||||
const commands = sequence(job["steps"], "jobs.check.steps")
|
||||
.map((step) => (typeof step["run"] === "string" ? step["run"] : ""))
|
||||
const job = mapping(mapping(workflow.jobs, "jobs").check, "jobs.check");
|
||||
const commands = sequence(job.steps, "jobs.check.steps")
|
||||
.map((step) => (typeof step.run === "string" ? step.run : ""))
|
||||
.join("\n");
|
||||
// `vp check` type-checks every phase, and `vp test` runs each of them under
|
||||
// Node as well as the root-only isolation tests.
|
||||
@@ -175,8 +175,8 @@ test("profile preparation does not receive provider credentials", async () => {
|
||||
|
||||
test("central profiles fall back from Codex to Claude to OpenCode", async () => {
|
||||
const credentials: unknown = JSON.parse(await readFile("profiles.json", "utf8"));
|
||||
const profiles = sequence(mapping(credentials, "profiles.json")["profiles"], "profiles");
|
||||
expect(profiles.map((profile) => profile["type"])).toStrictEqual(["codex", "claude", "opencode"]);
|
||||
expect(profiles[2]?.["model"]).toBe("openrouter/qwen/qwen3.8-27b");
|
||||
assert(String(profiles[2]?.["token"]).startsWith("v1:"));
|
||||
const profiles = sequence(mapping(credentials, "profiles.json").profiles, "profiles");
|
||||
expect(profiles.map((profile) => profile.type)).toStrictEqual(["codex", "claude", "opencode"]);
|
||||
expect(profiles[2]?.model).toBe("openrouter/qwen/qwen3.8-27b");
|
||||
assert(String(profiles[2]?.token).startsWith("v1:"));
|
||||
});
|
||||
|
||||
+4
-2
@@ -9,12 +9,14 @@
|
||||
"erasableSyntaxOnly": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
"noUnusedParameters": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user