Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0920a4e91b | |||
| 627458fb5f | |||
| 50df193a8c | |||
| 5b568f9ea6 | |||
| 60c63dee98 | |||
| 0a31dbb820 | |||
| 6ad859be62 |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: bot-agents migration decisions: live @main action for six repos, JSON profiles, Node 24.2+, Deno kept only for agents, no @cunarist plugins needed
|
||||
description: bot-agents migration decisions: live @main action for six repos, JSON profiles, Node 24.2+, Deno kept only for agents, no eslint-plugin-* packages needed
|
||||
---
|
||||
|
||||
Decisions for `commons/bot-agents` (Capsulizers Gitea), from the Deno to
|
||||
@@ -15,17 +15,20 @@ Node + Vite+ migration (PR #20, `node-migration`, 2026-10-15/16):
|
||||
removed the only runtime dependency (`@std/yaml`); keep it free of runtime
|
||||
dependencies (`@eslint/core` and the other devDependencies serve only the
|
||||
tooling).
|
||||
- **Node 24.2+**, not 22: `src/post.ts` uses `import.meta.main`. `engines.node`
|
||||
is `>=24.2`, which the user accepted for this repo
|
||||
([[bot-agents-runtime-no-build]]).
|
||||
- **Node 24.2+**, not 22: `src/post.ts` uses `import.meta.main`. There is no
|
||||
`engines` (user decision 2026-10-18): the `node:24-bookworm` job image
|
||||
decides the version ([[bot-agents-runtime-no-build]]).
|
||||
- **Deno stays in the action's global npm install** (`action.yml`), only
|
||||
because the agents still check Deno-based consumer repos (memona,
|
||||
memona-home). Remove it once those repos have moved; it is not a
|
||||
dependency of this repo's TypeScript.
|
||||
- **No `@cunarist/eslint-plugin-*`.** The Deno setup enabled no plugin, so
|
||||
this repo depends on none and CI passes without them being published.
|
||||
Planned (2026-10-17): once the plugins are on npm, add
|
||||
`@cunarist/eslint-plugin-modules` here for its import checks.
|
||||
- **No `eslint-plugin-*` packages** (`eslint-plugin-explicit-types`,
|
||||
`eslint-plugin-import-structure`, `eslint-plugin-lit-robust`; unscoped
|
||||
since 2026-10-18, formerly `@cunarist/...`). The Deno setup enabled no
|
||||
plugin, so this repo depends on none and CI passes without them being
|
||||
published. Planned (2026-10-17): once the plugins are on npm, add
|
||||
`eslint-plugin-import-structure` (rule prefix `import-structure/`) here for
|
||||
its import checks.
|
||||
- **Stricter input checks were accepted** in the migration: Gitea `/user` and
|
||||
comment responses, the runtime manifest, and the outcome's profile type are
|
||||
validated; some type errors became `TypeError` with the same messages.
|
||||
|
||||
@@ -10,8 +10,9 @@ dependency**. Hence `tsconfig.json` has `erasableSyntaxOnly` and
|
||||
properties, no decorators) and relative `.ts` imports.
|
||||
|
||||
- **Image:** the steps assume the `node:24-bookworm` job container. Node must
|
||||
be 24.2+ for `import.meta.main` in `post.ts`; `check.yml` prints
|
||||
`node --version` to confirm the cached image's minor.
|
||||
be 24.2+ for `import.meta.main` in `post.ts`. That image is the only Node
|
||||
pin (`package.json` has no `engines`); `check.yml` prints `node --version`
|
||||
to confirm the cached image's minor.
|
||||
- **Root-owned source copy:** `scripts/protect-source.sh` copies the action
|
||||
into `$RUNNER_TEMP` so an agent editing the checkout can't swap a later
|
||||
phase. It copies exactly `src/*.ts`, `package.json` (`"type": "module"`
|
||||
|
||||
@@ -11,23 +11,25 @@ unbuilt. `vp pack` inlines them, so they never reach package consumers.
|
||||
|
||||
One rule for every file, never tied to folder or file names (user rule
|
||||
2026-10-17; it replaces the earlier "aliases only point at direct `src/`
|
||||
subfolders" rule as the general rule, and the "modules plugin checks only
|
||||
aliased folders" scope; this repo keeps the stricter rule as a local
|
||||
convention, see the last section):
|
||||
subfolders" rule as the general rule, and the "import-structure plugin
|
||||
checks only aliased folders" scope; this repo keeps the stricter rule as a
|
||||
local convention, see the last section):
|
||||
|
||||
- **An alias may point anywhere** and owns its target. An alias to a
|
||||
folder's `index.ts` owns that folder and everything below it; an alias to
|
||||
any other file owns only that file.
|
||||
- **No overlap.** No file may be owned by 2 aliases: no alias inside another
|
||||
alias's area, and no 2 aliases with the same target. The modules plugin's
|
||||
`enforce-layer-order` reports it once at the top of every file 2 aliases
|
||||
claim. Fix by merging the aliases or moving files apart.
|
||||
alias's area, and no 2 aliases with the same target. The import-structure
|
||||
plugin's `enforce-layer-order` reports it once at the top of every file 2
|
||||
aliases claim. Fix by merging the aliases or moving files apart.
|
||||
- **Outside an area, only the exact alias reaches it.** A deeper alias
|
||||
specifier (`#a/one.ts`, `no-barrel-bypass`) and a relative path into the
|
||||
area (`./a/one.ts`, `no-relative-bypass`; `./b/f.ts` to a file alias,
|
||||
`prefer-alias-import`, fixed to the alias) are bypasses.
|
||||
- **Inside an area, relative imports of siblings and subfolders are free.**
|
||||
- **`../` is banned in every file**, owned or not (`no-parent-import`).
|
||||
- **A `../` import is banned in every file**, owned or not (`no-parent-import`).
|
||||
Runtime file URLs (`new URL("../prompt.md", import.meta.url)`) are not
|
||||
imports and are allowed.
|
||||
Anything above a file needs an alias.
|
||||
- **Files no alias owns** need no `index.ts` and no alias, and import `./`
|
||||
siblings and subfolders freely.
|
||||
@@ -89,8 +91,8 @@ Relative imports are written with the `.ts` extension.
|
||||
## Local convention: aliases target direct `src/` subfolders
|
||||
|
||||
This is a convention of this repo only, stricter than the general rule above
|
||||
(which the published modules plugin checks and which allows more). It is not
|
||||
a plugin rule.
|
||||
(which the published import-structure plugin checks and which allows more).
|
||||
It is not a plugin rule.
|
||||
|
||||
- Every `package.json` `imports` alias points only at the `index.ts` of a
|
||||
direct subfolder of `src/`, e.g. `"#helpers": "./src/helpers/index.ts"`.
|
||||
|
||||
@@ -8,7 +8,7 @@ turn on whole categories, then list only what parity or a real need adds.
|
||||
```ts
|
||||
lint: {
|
||||
plugins: ["typescript", "unicorn", "oxc", "import"],
|
||||
jsPlugins: ["@cunarist/eslint-plugin-explicit-types", "./scripts/project-lint.ts"],
|
||||
jsPlugins: ["eslint-plugin-explicit-types", "./scripts/project-lint.ts"],
|
||||
categories: { correctness: "error", suspicious: "error", pedantic: "error", perf: "error" },
|
||||
rules: { ...explicitTypes.configs.concrete.rules, ...projectLint.configs.project.rules },
|
||||
ignorePatterns: ["src/generated/**"], // generated files only, see below
|
||||
@@ -40,8 +40,8 @@ lint: {
|
||||
Hand-written code is never ignored.
|
||||
- **No lint disable comments**: [[lint-no-disable-comments]].
|
||||
- **Parity:** [[lint-parity-never-shrinks]]. **Naming:** [[naming-id-match]].
|
||||
- Don't add a second import sorter; the modules plugin's `enforce-import-order` is
|
||||
the only one, and Oxfmt's `sortImports` stays off.
|
||||
- Don't add a second import sorter; the import-structure plugin's
|
||||
`enforce-import-order` is the only one, and Oxfmt's `sortImports` stays off.
|
||||
|
||||
`perf` came last: in the template it flagged nine `.includes` on constant
|
||||
arrays, fixed as `ReadonlySet` + `.has()` rather than by turning a rule off.
|
||||
|
||||
@@ -9,19 +9,21 @@ Oxfmt's standard `package.json` sorting is accepted: there is no
|
||||
replaces the earlier hand-written order (metadata, `scripts`, `exports`,
|
||||
`imports`, `devDependencies`, `dependencies`).
|
||||
|
||||
The `imports` key order can carry meaning: the modules plugin's
|
||||
The `imports` key order can carry meaning: the import-structure plugin's
|
||||
`enforce-layer-order` reads it as the layer order. Never reorder it casually.
|
||||
|
||||
## Fields
|
||||
|
||||
- **Standard fields only.** `engines` has only `node` (never `engines.bun`).
|
||||
- **Standard fields only.** App repos have no `engines` field (user decision
|
||||
2026-10-18; images pin Node, [[toolchain-node-vite-plus]]). Published
|
||||
packages keep `engines` with only `node` (never `engines.bun`).
|
||||
A tool-owned key (`eslint-doc-generator`) is allowed only because the tool
|
||||
reads nothing else and no other config file is allowed.
|
||||
- **No `packageManager`.** npm is detected from `package-lock.json`.
|
||||
- **No `peerDependencies`, ever** (user preference, 2026-10-14). If something
|
||||
is needed at runtime, it goes in `dependencies`.
|
||||
- **`dependencies`** = what runs or is type-referenced at runtime by consumers
|
||||
(the lit plugin ships `parse5`, `oxc-parser`, `oxc-resolver`, and
|
||||
(the lit-robust plugin ships `parse5`, `oxc-parser`, `oxc-resolver`, and
|
||||
`@eslint/core` for its public types; they stay external, never bundled).
|
||||
- **`devDependencies`** = everything else: `vite-plus`, `eslint`,
|
||||
`@typescript-eslint/parser`, `eslint-doc-generator`, `esbuild`,
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
---
|
||||
description: Node.js 22+ with Vite+ (vp) and npm; only package.json, tsconfig.json, vite.config.ts plus a committed package-lock.json
|
||||
description: Node.js (no engines in apps; images pin 24) with Vite+ (vp) and npm; only package.json, tsconfig.json, vite.config.ts plus a committed package-lock.json
|
||||
---
|
||||
|
||||
## The stack
|
||||
|
||||
- **Runtime:** Node.js 22 or newer. `engines` is `{ "node": ">=22" }` and
|
||||
nothing else. Containers and the publish job use Node 24 (LTS); a repo may
|
||||
need more where a feature demands it (bot-agents uses Node 24.2+).
|
||||
- **Runtime:** Node.js. App repos have no `engines` field (user decision
|
||||
2026-10-18): the container or runner image decides the version, so the
|
||||
Containerfile base image (`node:24-alpine`) and CI's `node-version: "24"`
|
||||
are the pins. Published packages (the lint plugins) keep their own
|
||||
`engines.node` floor. `npm run lit-check` runs
|
||||
`node scripts/lit-check.ts`, which needs Node 22.18+ (unflagged type
|
||||
stripping and `module.registerHooks`); Node 24 satisfies it. Without
|
||||
`engines` or a `.node-version`, local `vp` uses its default Node
|
||||
(`vp env default`). bot-agents has no Containerfile or setup step: the
|
||||
self-hosted runner's `node:24-bookworm` job image runs the action and its
|
||||
checks, and `src/post.ts` needs 24.2+ (`import.meta.main`).
|
||||
- **Bun is not supported** (dropped 2026-10-15). No Bun CI legs, docs, or
|
||||
runtime branches. Oxlint's `RuleTester` refuses to run under Bun anyway, and
|
||||
`vp` brings its own Node, the way uv brings its own Python.
|
||||
|
||||
@@ -6,11 +6,17 @@ 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 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.
|
||||
`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`, and `allowUnreachableCode: false` plus
|
||||
`allowUnusedLabels: false` (user decision, 2026-10-18; on in the Deno setup),
|
||||
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.
|
||||
|
||||
## No asset imports (2026-10-14, widened 2026-10-16 and 2026-10-17)
|
||||
|
||||
|
||||
@@ -13,11 +13,15 @@ explicit permission.
|
||||
# Runtime and checks
|
||||
|
||||
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
|
||||
newer; `package.json` has no `engines`, the `node:24-bookworm` image pins it),
|
||||
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. 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. 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/`.
|
||||
|
||||
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
|
||||
@@ -25,16 +29,17 @@ below it; an alias to any other file owns only that file. No overlap: no file
|
||||
may be owned by 2 aliases, so no alias inside another alias's area and no 2
|
||||
aliases with the same target. Outside an area, only the exact alias reaches
|
||||
it; a deeper alias specifier or a relative path into the area is a bypass.
|
||||
Inside an area, relative imports of siblings and subfolders are free. `../`
|
||||
is banned in every file, owned or not
|
||||
(`import/no-relative-parent-imports`). Files no alias owns need no `index.ts`
|
||||
and no alias, and import `./` siblings and subfolders freely. The one alias
|
||||
is `#testing` (`./src/testing/index.ts`): every test imports it by alias, and
|
||||
`tests/` reaches `src/` only through it. Local convention, stricter than the
|
||||
general rule above: in this repo, aliases point only at a direct subfolder of
|
||||
`src/` (`"#helpers": "./src/helpers/index.ts"`), and code outside `src/` uses
|
||||
`./` relative imports. `scripts/` is flat: one file per
|
||||
tool, no subfolders and no `index.ts`, imported directly by file path
|
||||
Inside an area, relative imports of siblings and subfolders are free. A `../`
|
||||
import is banned in every file, owned or not
|
||||
(`import/no-relative-parent-imports`); runtime file URLs such as
|
||||
`new URL("../prompt.md", import.meta.url)` are paths, not imports, and stay.
|
||||
Files no alias owns need no `index.ts` and no alias, and import `./` siblings
|
||||
and subfolders freely. The one alias is `#testing` (`./src/testing/index.ts`):
|
||||
tests that use its helpers import it by alias, and `tests/` imports `src/`
|
||||
only through it. Local convention, stricter than the general rule above: in
|
||||
this repo, aliases point only at a direct subfolder of `src/`
|
||||
(`"#helpers": "./src/helpers/index.ts"`), and code outside `src/` uses `./`
|
||||
relative imports. `scripts/` is flat: one file per tool, no subfolders and no `index.ts`, imported directly by file path
|
||||
([.agents/memory/imports-aliases-barrels.md](.agents/memory/imports-aliases-barrels.md)).
|
||||
|
||||
The only config files are `package.json`, `tsconfig.json`, and
|
||||
|
||||
Generated
-3
@@ -11,9 +11,6 @@
|
||||
"@typescript-eslint/types": "^8.71.2",
|
||||
"vite-plus": "^1.2.0",
|
||||
"yaml": "^2.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
|
||||
@@ -12,8 +12,5 @@
|
||||
"@typescript-eslint/types": "^8.71.2",
|
||||
"vite-plus": "^1.2.0",
|
||||
"yaml": "^2.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.2"
|
||||
}
|
||||
}
|
||||
|
||||
+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",
|
||||
|
||||
+12
-13
@@ -85,7 +85,7 @@ async function gitea(path: string, body?: unknown): Promise<Response> {
|
||||
Authorization: `token ${AUTHOR_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body === undefined ? null : JSON.stringify(body),
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`${path}: ${response.status} ${await response.text()}`);
|
||||
@@ -104,11 +104,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> {
|
||||
@@ -151,7 +151,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`;
|
||||
}
|
||||
|
||||
@@ -228,24 +228,23 @@ 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 = {
|
||||
...(typeof isError === "boolean" && { is_error: isError }),
|
||||
...(typeof answer === "string" && { result: answer }),
|
||||
is_error: typeof isError === "boolean" ? isError : undefined,
|
||||
result: typeof answer === "string" ? answer : undefined,
|
||||
subtype: String(subtype),
|
||||
...((typeof apiStatus === "number" || apiStatus === null) && {
|
||||
api_error_status: apiStatus,
|
||||
}),
|
||||
api_error_status:
|
||||
typeof apiStatus === "number" || apiStatus === null ? apiStatus : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+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.
|
||||
@@ -170,8 +170,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:"));
|
||||
});
|
||||
|
||||
+1
-1
@@ -9,8 +9,8 @@
|
||||
"erasableSyntaxOnly": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
Reference in New Issue
Block a user