@asrunjs/asrun 0.0.0-stage → 0.5.2
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/.claude/commands/explore-case.md +18 -0
- package/.claude/commands/gen-spec.md +16 -0
- package/.claude/commands/heal-spec.md +26 -0
- package/.claude/commands/make-case.md +11 -0
- package/.claude/settings.json +5 -0
- package/.claude/skills/exploring-cases/SKILL.md +146 -0
- package/.claude/skills/exploring-cases/references/assertion-targets.md +103 -0
- package/.claude/skills/exploring-cases/references/handoff-card.js +128 -0
- package/.claude/skills/exploring-cases/references/open-questions.md +116 -0
- package/.claude/skills/exploring-cases/references/readiness.md +146 -0
- package/.claude/skills/exploring-cases/references/user-handoff.md +157 -0
- package/.claude/skills/generating-specs/SKILL.md +219 -0
- package/.claude/skills/generating-specs/references/conventions.md +148 -0
- package/.claude/skills/making-cases/SKILL.md +72 -0
- package/.claude/skills/making-cases/references/gaps.md +36 -0
- package/.claude/skills/making-cases/references/normalizing.md +39 -0
- package/.claude/skills/making-cases/references/splitting.md +28 -0
- package/.claude/skills/making-cases/references/writes-and-email.md +37 -0
- package/AGENTS.md +159 -0
- package/CLAUDE.md +2 -0
- package/LICENSE +21 -0
- package/README.md +511 -2
- package/bin/asrun.mjs +49 -0
- package/dist/lib/account-lock.d.ts +6 -0
- package/dist/lib/account-lock.js +52 -0
- package/dist/lib/assertions.d.ts +14 -0
- package/dist/lib/assertions.js +39 -0
- package/dist/lib/baselines.d.ts +43 -0
- package/dist/lib/baselines.js +108 -0
- package/dist/lib/build-artifacts.d.ts +8 -0
- package/dist/lib/build-artifacts.js +10 -0
- package/dist/lib/case-doc.d.ts +311 -0
- package/dist/lib/case-doc.js +1117 -0
- package/dist/lib/case-id.d.ts +11 -0
- package/dist/lib/case-id.js +17 -0
- package/dist/lib/case-ops.d.ts +26 -0
- package/dist/lib/case-ops.js +110 -0
- package/dist/lib/cases.d.ts +16 -0
- package/dist/lib/cases.js +51 -0
- package/dist/lib/cli.d.ts +10 -0
- package/dist/lib/cli.js +18 -0
- package/dist/lib/command.d.ts +24 -0
- package/dist/lib/command.js +90 -0
- package/dist/lib/downloads.d.ts +42 -0
- package/dist/lib/downloads.js +84 -0
- package/dist/lib/email.d.ts +10 -0
- package/dist/lib/email.js +19 -0
- package/dist/lib/ffmpeg.d.ts +8 -0
- package/dist/lib/ffmpeg.js +50 -0
- package/dist/lib/fixtures.d.ts +40 -0
- package/dist/lib/fixtures.js +149 -0
- package/dist/lib/login-card.d.ts +22 -0
- package/dist/lib/login-card.js +183 -0
- package/dist/lib/node-cli.d.ts +15 -0
- package/dist/lib/node-cli.js +36 -0
- package/dist/lib/paths.d.ts +25 -0
- package/dist/lib/paths.js +88 -0
- package/dist/lib/pdf.d.ts +71 -0
- package/dist/lib/pdf.js +140 -0
- package/dist/lib/profiles.d.ts +62 -0
- package/dist/lib/profiles.js +174 -0
- package/dist/lib/ready.d.ts +36 -0
- package/dist/lib/ready.js +53 -0
- package/dist/lib/records.d.ts +33 -0
- package/dist/lib/records.js +61 -0
- package/dist/lib/run-filters.d.ts +19 -0
- package/dist/lib/run-filters.js +38 -0
- package/dist/lib/run-summary.d.ts +90 -0
- package/dist/lib/run-summary.js +35 -0
- package/dist/lib/session.d.ts +30 -0
- package/dist/lib/session.js +111 -0
- package/dist/lib/summary-reporter.d.ts +16 -0
- package/dist/lib/summary-reporter.js +146 -0
- package/dist/lib/targets.d.ts +19 -0
- package/dist/lib/targets.js +26 -0
- package/dist/lib/ui-build.d.ts +10 -0
- package/dist/lib/ui-build.js +30 -0
- package/dist/lib/values.d.ts +29 -0
- package/dist/lib/values.js +85 -0
- package/dist/lib/write-gate.d.ts +41 -0
- package/dist/lib/write-gate.js +129 -0
- package/dist/playwright.config.d.ts +2 -0
- package/dist/playwright.config.js +92 -0
- package/dist/scripts/build.d.ts +1 -0
- package/dist/scripts/build.js +21 -0
- package/dist/scripts/check-cases.d.ts +1 -0
- package/dist/scripts/check-cases.js +149 -0
- package/dist/scripts/cleanup.d.ts +1 -0
- package/dist/scripts/cleanup.js +25 -0
- package/dist/scripts/cli.d.ts +1 -0
- package/dist/scripts/cli.js +28 -0
- package/dist/scripts/doctor.d.ts +1 -0
- package/dist/scripts/doctor.js +296 -0
- package/dist/scripts/explore.d.ts +1 -0
- package/dist/scripts/explore.js +75 -0
- package/dist/scripts/go.mjs +188 -0
- package/dist/scripts/heal-evidence.mjs +143 -0
- package/dist/scripts/init.d.ts +22 -0
- package/dist/scripts/init.js +332 -0
- package/dist/scripts/lint-specs.d.ts +1 -0
- package/dist/scripts/lint-specs.js +141 -0
- package/dist/scripts/login.d.ts +1 -0
- package/dist/scripts/login.js +75 -0
- package/dist/scripts/new-case.d.ts +1 -0
- package/dist/scripts/new-case.js +19 -0
- package/dist/scripts/render-pdf.d.ts +1 -0
- package/dist/scripts/render-pdf.js +44 -0
- package/dist/scripts/report.d.ts +1 -0
- package/dist/scripts/report.js +14 -0
- package/dist/scripts/review-baselines.d.ts +1 -0
- package/dist/scripts/review-baselines.js +30 -0
- package/dist/scripts/run.d.ts +1 -0
- package/dist/scripts/run.js +423 -0
- package/dist/scripts/status.d.ts +1 -0
- package/dist/scripts/status.js +31 -0
- package/dist/scripts/ui-build.d.ts +1 -0
- package/dist/scripts/ui-build.js +7 -0
- package/dist/scripts/ui-dev.d.ts +1 -0
- package/dist/scripts/ui-dev.js +3 -0
- package/dist/scripts/ui.d.ts +1 -0
- package/dist/scripts/ui.js +13 -0
- package/dist/ui/dev.d.ts +1 -0
- package/dist/ui/dev.js +36 -0
- package/dist/ui/server/actions.d.ts +95 -0
- package/dist/ui/server/actions.js +365 -0
- package/dist/ui/server/ai-models.d.ts +3 -0
- package/dist/ui/server/ai-models.js +347 -0
- package/dist/ui/server/ai-stream.d.ts +66 -0
- package/dist/ui/server/ai-stream.js +408 -0
- package/dist/ui/server/app.d.ts +4 -0
- package/dist/ui/server/app.js +395 -0
- package/dist/ui/server/cases.d.ts +23 -0
- package/dist/ui/server/cases.js +170 -0
- package/dist/ui/server/files.d.ts +5 -0
- package/dist/ui/server/files.js +67 -0
- package/dist/ui/server/index.d.ts +1 -0
- package/dist/ui/server/index.js +33 -0
- package/dist/ui/server/jobs.d.ts +80 -0
- package/dist/ui/server/jobs.js +317 -0
- package/dist/ui/server/profiles.d.ts +35 -0
- package/dist/ui/server/profiles.js +371 -0
- package/dist/ui/server/watch-files.d.ts +4 -0
- package/dist/ui/server/watch-files.js +36 -0
- package/dist/ui/shared/ai-commands.d.ts +88 -0
- package/dist/ui/shared/ai-commands.js +177 -0
- package/dist/ui/shared/case-id.d.ts +1 -0
- package/dist/ui/shared/case-id.js +1 -0
- package/dist/ui/shared/jobs.d.ts +6 -0
- package/dist/ui/shared/jobs.js +7 -0
- package/dist/ui/shared/product.d.ts +3 -0
- package/dist/ui/shared/product.js +3 -0
- package/dist/ui/shared/run-filters.d.ts +1 -0
- package/dist/ui/shared/run-filters.js +1 -0
- package/dist/ui/shared/site-address.d.ts +6 -0
- package/dist/ui/shared/site-address.js +11 -0
- package/dist/ui/shared/types.d.ts +371 -0
- package/dist/ui/shared/types.js +1 -0
- package/docs/agents/framework-dev.md +79 -0
- package/docs/theme-guide.html +922 -0
- package/package.json +109 -3
- package/templates/.claude/commands/explore-case.md +22 -0
- package/templates/.claude/commands/gen-spec.md +21 -0
- package/templates/.claude/commands/heal-spec.md +31 -0
- package/templates/.claude/commands/make-case.md +13 -0
- package/templates/.claude/settings.json +5 -0
- package/templates/.mcp.json +8 -0
- package/templates/AGENTS.md +64 -0
- package/templates/CLAUDE.md +1 -0
- package/templates/cases/_template.md +57 -0
- package/templates/gitignore +13 -0
- package/templates/opencode.jsonc +16 -0
- package/templates/package.json +31 -0
- package/templates/playwright.config.ts +3 -0
- package/templates/pnpm-workspace.yaml +5 -0
- package/templates/profiles.yaml +42 -0
- package/templates/seed.spec.ts +9 -0
- package/templates/tsconfig.json +14 -0
- package/ui/dist/apple-touch-icon.png +0 -0
- package/ui/dist/assets/apl-gfMMiTNf.js +1 -0
- package/ui/dist/assets/asciiarmor-C_eisQCp.js +1 -0
- package/ui/dist/assets/asn1-DCl_8MCW.js +1 -0
- package/ui/dist/assets/asterisk-BEn0Cwpx.js +1 -0
- package/ui/dist/assets/brainfuck-D5EjA2JK.js +1 -0
- package/ui/dist/assets/clike-xvONpD0Z.js +1 -0
- package/ui/dist/assets/clojure-BflJGmDX.js +1 -0
- package/ui/dist/assets/cmake-84nbDH0I.js +1 -0
- package/ui/dist/assets/cobol-DHLQUbN7.js +1 -0
- package/ui/dist/assets/coffeescript-De_zI4J0.js +1 -0
- package/ui/dist/assets/commonlisp-DgAKBe0r.js +1 -0
- package/ui/dist/assets/crystal-Bl6YKCG_.js +1 -0
- package/ui/dist/assets/css-CnaYoQib.js +1 -0
- package/ui/dist/assets/cypher-p9eYesGn.js +1 -0
- package/ui/dist/assets/d-DWkQyNnU.js +1 -0
- package/ui/dist/assets/diff-ChtP43wD.js +1 -0
- package/ui/dist/assets/dist-BGbe0-Aa.js +1 -0
- package/ui/dist/assets/dist-Ba-HC662.js +2 -0
- package/ui/dist/assets/dist-BtMLMthJ.js +1 -0
- package/ui/dist/assets/dist-Bw5-uxs9.js +1 -0
- package/ui/dist/assets/dist-BzulKuec.js +1 -0
- package/ui/dist/assets/dist-C-Oyo1ZT.js +1 -0
- package/ui/dist/assets/dist-C8MdhqgG.js +1 -0
- package/ui/dist/assets/dist-CG1pHn5m.js +1 -0
- package/ui/dist/assets/dist-CgIQYY1o.js +1 -0
- package/ui/dist/assets/dist-CmpteMYY.js +9 -0
- package/ui/dist/assets/dist-DDJNvivM.js +1 -0
- package/ui/dist/assets/dist-DF3F_I8N.js +1 -0
- package/ui/dist/assets/dist-DSy3dVf4.js +11 -0
- package/ui/dist/assets/dist-Dlorc3Sx.js +1 -0
- package/ui/dist/assets/dist-Dq6sW5Ur.js +1 -0
- package/ui/dist/assets/dist-Dr9crbBz.js +13 -0
- package/ui/dist/assets/dist-jjxQutEm.js +1 -0
- package/ui/dist/assets/dockerfile-DJkHQgkl.js +1 -0
- package/ui/dist/assets/dtd-DTF72YNO.js +1 -0
- package/ui/dist/assets/dylan-S7ucR4pR.js +1 -0
- package/ui/dist/assets/ebnf-D4c0_ac3.js +1 -0
- package/ui/dist/assets/ecl-BVIeWmwY.js +1 -0
- package/ui/dist/assets/eiffel-BEjRio4Q.js +1 -0
- package/ui/dist/assets/elm-Cshzl8qu.js +1 -0
- package/ui/dist/assets/erlang-BV0c4Hdn.js +1 -0
- package/ui/dist/assets/factor-LKH5gN8L.js +1 -0
- package/ui/dist/assets/fcl-ClOhbFR7.js +1 -0
- package/ui/dist/assets/forth-D6RzAl64.js +1 -0
- package/ui/dist/assets/fortran-Bt6PBEDR.js +1 -0
- package/ui/dist/assets/gas-BHEdbvp9.js +1 -0
- package/ui/dist/assets/gherkin-oBAE_ms0.js +1 -0
- package/ui/dist/assets/groovy-CUrWKPia.js +1 -0
- package/ui/dist/assets/haskell-BRsoo5mP.js +1 -0
- package/ui/dist/assets/haxe-DjVOVvNP.js +1 -0
- package/ui/dist/assets/http-CLVgA2GD.js +1 -0
- package/ui/dist/assets/idl-BUZw3wgd.js +1 -0
- package/ui/dist/assets/index-CTmnF0Wp.css +2 -0
- package/ui/dist/assets/index-P_Yo0ZZn.js +338 -0
- package/ui/dist/assets/javascript-lS2r2ifg.js +1 -0
- package/ui/dist/assets/julia-CQfgDRfP.js +1 -0
- package/ui/dist/assets/livescript-BXxG0Yva.js +1 -0
- package/ui/dist/assets/lua-DPCEkfD_.js +1 -0
- package/ui/dist/assets/mathematica-u8YMmbU0.js +1 -0
- package/ui/dist/assets/mbox-BEMVcqjs.js +1 -0
- package/ui/dist/assets/mirc-C9zpzAZU.js +1 -0
- package/ui/dist/assets/mllike-B45xgp2S.js +1 -0
- package/ui/dist/assets/modelica-CqHI_q-D.js +1 -0
- package/ui/dist/assets/mscgen-qgSLujhx.js +1 -0
- package/ui/dist/assets/mumps-BSzj6Xyz.js +1 -0
- package/ui/dist/assets/nginx-C-ORUuei.js +1 -0
- package/ui/dist/assets/nsis-viPvXFiq.js +1 -0
- package/ui/dist/assets/ntriples-CzMBulkp.js +1 -0
- package/ui/dist/assets/octave-D2Q8cUp1.js +1 -0
- package/ui/dist/assets/oz-BwTct_5T.js +1 -0
- package/ui/dist/assets/pascal-Dp_KdFgX.js +1 -0
- package/ui/dist/assets/perl-BkSkiI9m.js +1 -0
- package/ui/dist/assets/pig-AF4Hwhju.js +1 -0
- package/ui/dist/assets/powershell-D8WpQPaX.js +1 -0
- package/ui/dist/assets/properties-C357ku5U.js +1 -0
- package/ui/dist/assets/protobuf-D1ij_kNL.js +1 -0
- package/ui/dist/assets/pug-BqCHZ5xT.js +1 -0
- package/ui/dist/assets/puppet-BZyAS7I4.js +1 -0
- package/ui/dist/assets/python-B_1Jd8HM.js +1 -0
- package/ui/dist/assets/q-B9NhS7L_.js +1 -0
- package/ui/dist/assets/r-CxDmU1I4.js +1 -0
- package/ui/dist/assets/rpm-gpDK5sbt.js +1 -0
- package/ui/dist/assets/ruby-ChfNwlCY.js +1 -0
- package/ui/dist/assets/sas-C2OyD2Y6.js +1 -0
- package/ui/dist/assets/scheme-D9qYN0V5.js +1 -0
- package/ui/dist/assets/shell-DwuoZtxw.js +1 -0
- package/ui/dist/assets/sieve-tLWDfEkZ.js +1 -0
- package/ui/dist/assets/simple-mode-DXiLM1-G.js +1 -0
- package/ui/dist/assets/smalltalk-BLp5-jwC.js +1 -0
- package/ui/dist/assets/solr-B78wSQJQ.js +1 -0
- package/ui/dist/assets/sparql-Dz-mlCAC.js +1 -0
- package/ui/dist/assets/spreadsheet-CTIncYxj.js +1 -0
- package/ui/dist/assets/sql-DuQMFDvy.js +1 -0
- package/ui/dist/assets/stex-DNzd62Q_.js +1 -0
- package/ui/dist/assets/stylus-BOVgKnng.js +1 -0
- package/ui/dist/assets/swift-DKB6_1j6.js +1 -0
- package/ui/dist/assets/tcl-BGfruO1u.js +1 -0
- package/ui/dist/assets/textile-C4jVoHG1.js +1 -0
- package/ui/dist/assets/tiddlywiki-VlUiK86J.js +1 -0
- package/ui/dist/assets/tiki-CvUEFusT.js +1 -0
- package/ui/dist/assets/toml-BPTmHmyx.js +1 -0
- package/ui/dist/assets/troff-C2dAgh7P.js +1 -0
- package/ui/dist/assets/ttcn-PPSTk1ow.js +1 -0
- package/ui/dist/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
- package/ui/dist/assets/turtle-Cx3rYX2g.js +1 -0
- package/ui/dist/assets/vb-BthdM_4N.js +1 -0
- package/ui/dist/assets/vbscript-D9f6rSsU.js +1 -0
- package/ui/dist/assets/velocity-CqftRsjg.js +1 -0
- package/ui/dist/assets/verilog-CjI9Y6RI.js +1 -0
- package/ui/dist/assets/vhdl-kS471soi.js +1 -0
- package/ui/dist/assets/webidl-D5jiJvOU.js +1 -0
- package/ui/dist/assets/xquery-BKJx4mf9.js +1 -0
- package/ui/dist/assets/yacas-CXXgtw0p.js +1 -0
- package/ui/dist/assets/z80-Co3PbHdE.js +1 -0
- package/ui/dist/favicon.ico +0 -0
- package/ui/dist/favicon.svg +11 -0
- package/ui/dist/help/ai-tools.png +0 -0
- package/ui/dist/help/case-explore.png +0 -0
- package/ui/dist/help/case-generate.png +0 -0
- package/ui/dist/help/cases-memo.png +0 -0
- package/ui/dist/help/profile-form.png +0 -0
- package/ui/dist/help/profiles-login.png +0 -0
- package/ui/dist/help/reports.png +0 -0
- package/ui/dist/help/run-tests.png +0 -0
- package/ui/dist/index.html +23 -0
- package/ui/dist/logos/claude.svg +1 -0
- package/ui/dist/logos/opencode.svg +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { buildUi, uiIsBuilt } from '../lib/ui-build.js';
|
|
2
|
+
// An installed package ships the built app; the framework repository builds it (or refreshes it with `asrun ui --build`).
|
|
3
|
+
if (!uiIsBuilt() || process.argv.includes('--build')) {
|
|
4
|
+
const status = buildUi();
|
|
5
|
+
if (status === null) {
|
|
6
|
+
console.error('This asrun installation has no built web UI. Reinstall it: "pnpm add -D @asrunjs/asrun".');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
if (status !== 0)
|
|
10
|
+
process.exit(status);
|
|
11
|
+
}
|
|
12
|
+
// Serves the built app and the API on one port (see ui/server/index.ts).
|
|
13
|
+
await import('../ui/server/index.js');
|
package/dist/ui/dev.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui/dev.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { commandScript } from '../lib/node-cli.js';
|
|
4
|
+
import { FRAMEWORK_ROOT, ROOT } from '../lib/paths.js';
|
|
5
|
+
const webPort = process.env.ASRUN_UI_WEB_PORT ?? '5173';
|
|
6
|
+
const tools = [
|
|
7
|
+
{
|
|
8
|
+
name: 'tsx',
|
|
9
|
+
script: commandScript('tsx'),
|
|
10
|
+
args: ['watch', path.join(FRAMEWORK_ROOT, 'ui', 'server', 'index.ts')],
|
|
11
|
+
env: { ...process.env, ASRUN_UI_DEV_ORIGIN: `http://127.0.0.1:${webPort}` },
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'vite',
|
|
15
|
+
script: commandScript('vite'),
|
|
16
|
+
args: ['--config', path.join(FRAMEWORK_ROOT, 'ui', 'web', 'vite.config.ts')],
|
|
17
|
+
env: process.env,
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
const children = [];
|
|
21
|
+
const stop = () => children.forEach((child) => child.kill('SIGTERM'));
|
|
22
|
+
process.on('SIGINT', stop);
|
|
23
|
+
process.on('SIGTERM', stop);
|
|
24
|
+
for (const tool of tools) {
|
|
25
|
+
const child = spawn(process.execPath, [tool.script, ...tool.args], { cwd: ROOT, stdio: 'inherit', env: tool.env });
|
|
26
|
+
children.push(child);
|
|
27
|
+
child.on('error', (error) => {
|
|
28
|
+
console.error(`Failed to start ${tool.name}: ${error.message}`);
|
|
29
|
+
stop();
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
child.on('exit', (code) => {
|
|
33
|
+
stop();
|
|
34
|
+
process.exit(code ?? 0);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { RunPlan } from '../shared/types.js';
|
|
3
|
+
import { type JobSpec } from './jobs.js';
|
|
4
|
+
export declare const startJobSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
5
|
+
action: z.ZodLiteral<"signin">;
|
|
6
|
+
profile: z.ZodString;
|
|
7
|
+
env: z.ZodString;
|
|
8
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
9
|
+
action: z.ZodLiteral<"visit">;
|
|
10
|
+
profile: z.ZodString;
|
|
11
|
+
env: z.ZodString;
|
|
12
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
13
|
+
action: z.ZodLiteral<"ai">;
|
|
14
|
+
tool: z.ZodEnum<{
|
|
15
|
+
claude: "claude";
|
|
16
|
+
opencode: "opencode";
|
|
17
|
+
}>;
|
|
18
|
+
command: z.ZodEnum<{
|
|
19
|
+
"explore-case": "explore-case";
|
|
20
|
+
"gen-spec": "gen-spec";
|
|
21
|
+
"heal-spec": "heal-spec";
|
|
22
|
+
}>;
|
|
23
|
+
caseId: z.ZodString;
|
|
24
|
+
env: z.ZodString;
|
|
25
|
+
force: z.ZodOptional<z.ZodBoolean>;
|
|
26
|
+
attended: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
thinking: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
+
model: z.ZodOptional<z.ZodString>;
|
|
29
|
+
profiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
30
|
+
hints: z.ZodOptional<z.ZodString>;
|
|
31
|
+
report: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
33
|
+
action: z.ZodLiteral<"continue">;
|
|
34
|
+
jobId: z.ZodString;
|
|
35
|
+
reply: z.ZodString;
|
|
36
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
37
|
+
action: z.ZodLiteral<"make-cases">;
|
|
38
|
+
tool: z.ZodEnum<{
|
|
39
|
+
claude: "claude";
|
|
40
|
+
opencode: "opencode";
|
|
41
|
+
}>;
|
|
42
|
+
memo: z.ZodString;
|
|
43
|
+
model: z.ZodOptional<z.ZodString>;
|
|
44
|
+
thinking: z.ZodOptional<z.ZodBoolean>;
|
|
45
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
46
|
+
action: z.ZodLiteral<"run">;
|
|
47
|
+
env: z.ZodString;
|
|
48
|
+
profiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
49
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
50
|
+
suites: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
51
|
+
grep: z.ZodOptional<z.ZodString>;
|
|
52
|
+
stepText: z.ZodOptional<z.ZodBoolean>;
|
|
53
|
+
readOnly: z.ZodOptional<z.ZodBoolean>;
|
|
54
|
+
updateBaselines: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
|
+
confirmWritesIn: z.ZodOptional<z.ZodString>;
|
|
56
|
+
}, z.core.$strip>], "action">;
|
|
57
|
+
export declare const previewSchema: z.ZodObject<{
|
|
58
|
+
env: z.ZodString;
|
|
59
|
+
profiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
60
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
61
|
+
suites: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
62
|
+
grep: z.ZodOptional<z.ZodString>;
|
|
63
|
+
readOnly: z.ZodOptional<z.ZodBoolean>;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
export declare class ActionError extends Error {
|
|
66
|
+
}
|
|
67
|
+
/** The same filters the run will use, so the preview and the real run select the same cases. */
|
|
68
|
+
export declare function runFilters(request: {
|
|
69
|
+
env: string;
|
|
70
|
+
profiles?: string[];
|
|
71
|
+
tags?: string[];
|
|
72
|
+
suites?: string[];
|
|
73
|
+
grep?: string;
|
|
74
|
+
readOnly?: boolean;
|
|
75
|
+
}): {
|
|
76
|
+
env: string;
|
|
77
|
+
profiles: string[] | null;
|
|
78
|
+
suites: string[];
|
|
79
|
+
grep: string | undefined;
|
|
80
|
+
readOnly: boolean;
|
|
81
|
+
};
|
|
82
|
+
/** What a run would do: the cases that write data and whether the user has to confirm them first. */
|
|
83
|
+
export declare function previewRun(request: z.infer<typeof previewSchema>): {
|
|
84
|
+
env: string;
|
|
85
|
+
needsConfirmation: boolean;
|
|
86
|
+
writeCases: import("../../lib/write-gate.js").WriteCase[];
|
|
87
|
+
skippedWriteCases: import("../../lib/write-gate.js").WriteCase[];
|
|
88
|
+
};
|
|
89
|
+
/** The cases a run with these filters would execute, for the Run page. A failure of the list is reported, not thrown. */
|
|
90
|
+
export declare function planRunCases(request: z.infer<typeof previewSchema>): Promise<RunPlan>;
|
|
91
|
+
/**
|
|
92
|
+
* The only way to run something: a fixed list of actions that map to the repo's own scripts.
|
|
93
|
+
* Every value that ends up in argv is checked against profiles.yaml or the cases first.
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildJobSpec(request: z.infer<typeof startJobSchema>): JobSpec;
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { listEnvs, listProfiles, loadProfiles } from '../../lib/profiles.js';
|
|
6
|
+
import { listSuites } from '../../lib/cases.js';
|
|
7
|
+
import { findWriteCases, isProtectedEnv, planRun } from '../../lib/write-gate.js';
|
|
8
|
+
import { scriptArgs } from '../../lib/node-cli.js';
|
|
9
|
+
import { ROOT } from '../../lib/paths.js';
|
|
10
|
+
import { findContinuation } from '../shared/jobs.js';
|
|
11
|
+
import { jobs } from './jobs.js';
|
|
12
|
+
import { verifyProfiles } from './profiles.js';
|
|
13
|
+
import { isValidCaseId, listCaseIds } from './cases.js';
|
|
14
|
+
import { snapshotDirPath, specFilePath } from '../../lib/case-ops.js';
|
|
15
|
+
import { AI_COMMANDS, AI_COMMAND_WRITES, AI_TOOLS, continueCommandLine, continueInvocation, headlessCommandLine, headlessInvocation, HINTS_DIR, HINTS_MAX_CHARS, makeCaseCommandLine, makeCaseInvocation, MEMO_DIR, MEMO_MAX_CHARS, REPLY_DIR, REPLY_MAX_CHARS, } from '../shared/ai-commands.js';
|
|
16
|
+
const REPORTS_DIR = path.join(ROOT, 'reports');
|
|
17
|
+
const REPORT_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-[a-z0-9]+(-[a-z0-9]+)*)?$/;
|
|
18
|
+
const newSessionId = (tool) => tool === 'claude' ? crypto.randomUUID() : `ses_${crypto.randomBytes(12).toString('hex')}`;
|
|
19
|
+
export const startJobSchema = z.discriminatedUnion('action', [
|
|
20
|
+
z.object({ action: z.literal('signin'), profile: z.string().min(1), env: z.string().min(1) }),
|
|
21
|
+
z.object({ action: z.literal('visit'), profile: z.string().min(1), env: z.string().min(1) }),
|
|
22
|
+
z.object({
|
|
23
|
+
action: z.literal('ai'),
|
|
24
|
+
tool: z.enum(AI_TOOLS),
|
|
25
|
+
command: z.enum(AI_COMMANDS),
|
|
26
|
+
caseId: z.string().min(1).max(200),
|
|
27
|
+
env: z.string().min(1),
|
|
28
|
+
force: z.boolean().optional(),
|
|
29
|
+
attended: z.boolean().optional(),
|
|
30
|
+
thinking: z.boolean().optional(),
|
|
31
|
+
model: z.string().min(1).max(200).optional(),
|
|
32
|
+
profiles: z.array(z.string().min(1)).optional(),
|
|
33
|
+
hints: z.string().max(HINTS_MAX_CHARS).optional(),
|
|
34
|
+
report: z.string().min(1).max(100).optional(),
|
|
35
|
+
}),
|
|
36
|
+
z.object({
|
|
37
|
+
action: z.literal('continue'),
|
|
38
|
+
jobId: z.string().min(1).max(40),
|
|
39
|
+
reply: z.string().trim().min(1).max(REPLY_MAX_CHARS),
|
|
40
|
+
}),
|
|
41
|
+
z.object({
|
|
42
|
+
action: z.literal('make-cases'),
|
|
43
|
+
tool: z.enum(AI_TOOLS),
|
|
44
|
+
memo: z.string().max(MEMO_MAX_CHARS),
|
|
45
|
+
model: z.string().min(1).max(200).optional(),
|
|
46
|
+
thinking: z.boolean().optional(),
|
|
47
|
+
}),
|
|
48
|
+
z.object({
|
|
49
|
+
action: z.literal('run'),
|
|
50
|
+
env: z.string().min(1),
|
|
51
|
+
profiles: z.array(z.string().min(1)).optional(),
|
|
52
|
+
tags: z.array(z.string().min(1)).optional(),
|
|
53
|
+
suites: z.array(z.string().min(1)).optional(),
|
|
54
|
+
grep: z.string().max(200).optional(),
|
|
55
|
+
stepText: z.boolean().optional(),
|
|
56
|
+
readOnly: z.boolean().optional(),
|
|
57
|
+
updateBaselines: z.array(z.string().min(1).max(200)).max(200).optional(),
|
|
58
|
+
confirmWritesIn: z.string().optional(),
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
export const previewSchema = z.object({
|
|
62
|
+
env: z.string().min(1),
|
|
63
|
+
profiles: z.array(z.string().min(1)).optional(),
|
|
64
|
+
tags: z.array(z.string().min(1)).optional(),
|
|
65
|
+
suites: z.array(z.string().min(1)).optional(),
|
|
66
|
+
grep: z.string().max(200).optional(),
|
|
67
|
+
readOnly: z.boolean().optional(),
|
|
68
|
+
});
|
|
69
|
+
export class ActionError extends Error {
|
|
70
|
+
}
|
|
71
|
+
/** The same filters the run will use, so the preview and the real run select the same cases. */
|
|
72
|
+
export function runFilters(request) {
|
|
73
|
+
const known = loadProfiles();
|
|
74
|
+
const knownIds = known.map((profile) => profile.id);
|
|
75
|
+
const profilesInEnv = listProfiles(request.env, known);
|
|
76
|
+
const candidates = request.tags && request.tags.length > 0
|
|
77
|
+
? profilesInEnv.filter((profile) => profile.tags.some((tag) => request.tags.includes(tag)))
|
|
78
|
+
: profilesInEnv;
|
|
79
|
+
const available = candidates.map((profile) => profile.id);
|
|
80
|
+
const selected = request.profiles ?? available;
|
|
81
|
+
if (selected.some((id) => !knownIds.includes(id) || !available.includes(id))) {
|
|
82
|
+
throw new ActionError(`Unknown profile for "${request.env}".`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
env: request.env,
|
|
86
|
+
profiles: selected.length < available.length ? selected : null,
|
|
87
|
+
suites: request.suites ?? [],
|
|
88
|
+
grep: request.grep,
|
|
89
|
+
readOnly: request.readOnly ?? false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** What a run would do: the cases that write data and whether the user has to confirm them first. */
|
|
93
|
+
export function previewRun(request) {
|
|
94
|
+
if (!listEnvs().includes(request.env))
|
|
95
|
+
throw new ActionError(`Unknown environment "${request.env}".`);
|
|
96
|
+
const { writing, skipped } = request.readOnly ? { writing: [], skipped: [] } : findWriteCases(runFilters(request));
|
|
97
|
+
return {
|
|
98
|
+
env: request.env,
|
|
99
|
+
needsConfirmation: writing.length > 0 && isProtectedEnv(request.env),
|
|
100
|
+
writeCases: writing,
|
|
101
|
+
skippedWriteCases: skipped,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const countBaselines = (caseId) => {
|
|
105
|
+
const dir = snapshotDirPath(caseId);
|
|
106
|
+
return fs.existsSync(dir) ? fs.readdirSync(dir).filter((name) => name.toLowerCase().endsWith('.png')).length : 0;
|
|
107
|
+
};
|
|
108
|
+
/** The cases a run with these filters would execute, for the Run page. A failure of the list is reported, not thrown. */
|
|
109
|
+
export async function planRunCases(request) {
|
|
110
|
+
if (!listEnvs().includes(request.env))
|
|
111
|
+
throw new ActionError(`Unknown environment "${request.env}".`);
|
|
112
|
+
try {
|
|
113
|
+
const cases = (await planRun(runFilters(request))).map((item) => ({ ...item, baselineCount: countBaselines(item.id) }));
|
|
114
|
+
const running = cases.filter((item) => item.skip === null);
|
|
115
|
+
return {
|
|
116
|
+
env: request.env,
|
|
117
|
+
error: null,
|
|
118
|
+
totalCases: running.length,
|
|
119
|
+
totalTests: running.reduce((sum, item) => sum + item.profiles.length, 0),
|
|
120
|
+
cases,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof ActionError)
|
|
125
|
+
throw error;
|
|
126
|
+
return { env: request.env, error: error.message, totalCases: 0, totalTests: 0, cases: [] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const authLock = (profileId) => `auth:${profileId}`;
|
|
130
|
+
/**
|
|
131
|
+
* The only way to run something: a fixed list of actions that map to the repo's own scripts.
|
|
132
|
+
* Every value that ends up in argv is checked against profiles.yaml or the cases first.
|
|
133
|
+
*/
|
|
134
|
+
export function buildJobSpec(request) {
|
|
135
|
+
const knownProfiles = loadProfiles();
|
|
136
|
+
const knownProfileIds = knownProfiles.map((profile) => profile.id);
|
|
137
|
+
const knownEnvs = listEnvs(knownProfiles);
|
|
138
|
+
const checkProfile = (profileId, env) => {
|
|
139
|
+
if (!knownProfileIds.includes(profileId))
|
|
140
|
+
throw new ActionError(`Unknown profile "${profileId}".`);
|
|
141
|
+
if (!knownEnvs.includes(env))
|
|
142
|
+
throw new ActionError(`Unknown environment "${env}".`);
|
|
143
|
+
const profile = knownProfiles.find((candidate) => candidate.id === profileId);
|
|
144
|
+
if (profile?.env !== env) {
|
|
145
|
+
throw new ActionError(`Profile "${profileId}" belongs to ${profile?.env ?? 'another environment'}, not "${env}".`);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const allLocks = () => knownProfiles.map((profile) => authLock(profile.id));
|
|
149
|
+
if (request.action === 'signin') {
|
|
150
|
+
checkProfile(request.profile, request.env);
|
|
151
|
+
return {
|
|
152
|
+
kind: 'signin',
|
|
153
|
+
profile: request.profile,
|
|
154
|
+
env: request.env,
|
|
155
|
+
command: `pnpm signin ${request.profile}`,
|
|
156
|
+
file: process.execPath,
|
|
157
|
+
args: [...scriptArgs('login.ts'), request.profile],
|
|
158
|
+
locks: [authLock(request.profile)],
|
|
159
|
+
acceptsInput: true,
|
|
160
|
+
onFinish: () => void verifyProfiles({ ids: [request.profile] }),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (request.action === 'visit') {
|
|
164
|
+
checkProfile(request.profile, request.env);
|
|
165
|
+
// Where a login lands is a profile setting (its own home page), not something the UI knows.
|
|
166
|
+
const startPath = knownProfiles.find((profile) => profile.id === request.profile)?.startPath ?? '';
|
|
167
|
+
// Only reads the saved session when it starts, so it does not take the auth lock
|
|
168
|
+
// (that would block test runs for as long as the window stays open).
|
|
169
|
+
return {
|
|
170
|
+
kind: 'visit',
|
|
171
|
+
profile: request.profile,
|
|
172
|
+
env: request.env,
|
|
173
|
+
command: ['pnpm explore', request.profile, startPath].filter(Boolean).join(' '),
|
|
174
|
+
file: process.execPath,
|
|
175
|
+
args: [...scriptArgs('explore.ts'), request.profile, ...(startPath ? [startPath] : [])],
|
|
176
|
+
locks: [`visit:${request.profile}`],
|
|
177
|
+
acceptsInput: false,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// An AI run on a case, a first turn or a continuation: the same permissions and locks either way.
|
|
181
|
+
// Generating and healing write the spec and the case values and run the tests, so they must not overlap with a run.
|
|
182
|
+
// Reviewing only edits the case file and looks at pages, so it takes just the case lock.
|
|
183
|
+
const aiSpec = (tool, command, caseId, env, sessionId, run, model, thinking) => ({
|
|
184
|
+
kind: 'ai',
|
|
185
|
+
env,
|
|
186
|
+
...run,
|
|
187
|
+
locks: [`ai:${caseId}`, ...(AI_COMMAND_WRITES[command] === 'files' ? ['run', ...allLocks()] : [])],
|
|
188
|
+
acceptsInput: false,
|
|
189
|
+
stream: tool,
|
|
190
|
+
ai: { tool, command, caseId, env, sessionId, ...(model && { model }), ...(thinking && { thinking }) },
|
|
191
|
+
});
|
|
192
|
+
if (request.action === 'ai') {
|
|
193
|
+
if (!isValidCaseId(request.caseId) || !listCaseIds().includes(request.caseId)) {
|
|
194
|
+
throw new ActionError(`Unknown case "${request.caseId}".`);
|
|
195
|
+
}
|
|
196
|
+
// The environment is part of the prompt, so it is checked like every other value that reaches argv.
|
|
197
|
+
// Every command opens real pages, so there is always one and no default.
|
|
198
|
+
if (!knownEnvs.includes(request.env))
|
|
199
|
+
throw new ActionError(`Unknown environment "${request.env}".`);
|
|
200
|
+
// The server picks the conversation id, so the run can be continued after the agent asked its questions.
|
|
201
|
+
if (request.force && request.command !== 'gen-spec') {
|
|
202
|
+
throw new ActionError('Only gen-spec can overwrite an existing spec.');
|
|
203
|
+
}
|
|
204
|
+
// The agent leaves an existing spec alone unless told to overwrite it, so the user is asked first (the UI shows
|
|
205
|
+
// a dialog); refusing here saves a run that would do nothing.
|
|
206
|
+
if (request.command === 'gen-spec' && !request.force && fs.existsSync(specFilePath(request.caseId))) {
|
|
207
|
+
throw new ActionError(`A spec already exists for "${request.caseId}". Choose overwrite to regenerate it.`);
|
|
208
|
+
}
|
|
209
|
+
if (request.attended && request.command !== 'explore-case') {
|
|
210
|
+
throw new ActionError('Only explore-case can be attended.');
|
|
211
|
+
}
|
|
212
|
+
if ((request.profiles || request.hints !== undefined || request.report) && request.command !== 'heal-spec') {
|
|
213
|
+
throw new ActionError('Only heal-spec takes profiles, hints and a report.');
|
|
214
|
+
}
|
|
215
|
+
const profilesInEnv = listProfiles(request.env, knownProfiles).map((profile) => profile.id);
|
|
216
|
+
if (request.profiles?.some((id) => !profilesInEnv.includes(id))) {
|
|
217
|
+
throw new ActionError(`Unknown profile for "${request.env}".`);
|
|
218
|
+
}
|
|
219
|
+
if (request.profiles?.length === 0)
|
|
220
|
+
throw new ActionError('Choose at least one profile to heal.');
|
|
221
|
+
if (request.report !== undefined && !(REPORT_ID.test(request.report) && fs.existsSync(path.join(REPORTS_DIR, request.report, 'summary.json')))) {
|
|
222
|
+
throw new ActionError(`Unknown report "${request.report}".`);
|
|
223
|
+
}
|
|
224
|
+
// The clues are stored as a file so the agent reads them verbatim; the path is generated here, never taken from the browser.
|
|
225
|
+
let hintsPath;
|
|
226
|
+
if (request.hints !== undefined && request.hints.trim() !== '') {
|
|
227
|
+
hintsPath = `${HINTS_DIR}/${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomBytes(3).toString('hex')}.md`;
|
|
228
|
+
fs.mkdirSync(path.join(ROOT, HINTS_DIR), { recursive: true });
|
|
229
|
+
fs.writeFileSync(path.join(ROOT, hintsPath), request.hints.trim() + '\n');
|
|
230
|
+
}
|
|
231
|
+
const options = {
|
|
232
|
+
env: request.env,
|
|
233
|
+
model: request.model,
|
|
234
|
+
thinking: request.thinking,
|
|
235
|
+
force: request.force,
|
|
236
|
+
attended: request.attended,
|
|
237
|
+
// Naming every profile is the same as leaving it out: the agent then takes all that are logged in.
|
|
238
|
+
profiles: request.profiles && request.profiles.length < profilesInEnv.length ? request.profiles : undefined,
|
|
239
|
+
hints: hintsPath,
|
|
240
|
+
report: request.report,
|
|
241
|
+
};
|
|
242
|
+
const session = { id: newSessionId(request.tool), resume: false };
|
|
243
|
+
const { file, args } = headlessInvocation(request.tool, request.command, request.caseId, options, session);
|
|
244
|
+
return aiSpec(request.tool, request.command, request.caseId, request.env, session.id, {
|
|
245
|
+
command: headlessCommandLine(request.tool, request.command, request.caseId, options, session),
|
|
246
|
+
file,
|
|
247
|
+
args,
|
|
248
|
+
}, request.model, request.thinking);
|
|
249
|
+
}
|
|
250
|
+
if (request.action === 'continue') {
|
|
251
|
+
const earlier = jobs.get(request.jobId);
|
|
252
|
+
const ai = earlier?.spec.ai;
|
|
253
|
+
if (!earlier || !ai)
|
|
254
|
+
throw new ActionError('Unknown job, or it was not an AI run on a case.');
|
|
255
|
+
if (earlier.info.status !== 'succeeded' || earlier.info.resultState !== 'needs-input') {
|
|
256
|
+
throw new ActionError('Only a run that finished with RESULT: needs-input can be continued.');
|
|
257
|
+
}
|
|
258
|
+
if (findContinuation(jobs.list(), earlier.info.id)) {
|
|
259
|
+
throw new ActionError('This run was already continued. Continue the newer run instead.');
|
|
260
|
+
}
|
|
261
|
+
if (!listCaseIds().includes(ai.caseId))
|
|
262
|
+
throw new ActionError(`The case "${ai.caseId}" no longer exists.`);
|
|
263
|
+
// The answers are stored as a file so the agent reads them verbatim; a line break or a quote in them cannot
|
|
264
|
+
// break the command line of the agent CLI.
|
|
265
|
+
const replyPath = `${REPLY_DIR}/${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomBytes(3).toString('hex')}.md`;
|
|
266
|
+
fs.mkdirSync(path.join(ROOT, REPLY_DIR), { recursive: true });
|
|
267
|
+
fs.writeFileSync(path.join(ROOT, replyPath), request.reply.trim() + '\n');
|
|
268
|
+
// The next turn keeps how the first one was told to run (model and reasoning), so the log stays the same.
|
|
269
|
+
const { file, args } = continueInvocation(ai.tool, ai.command, replyPath, ai.sessionId, ai.model, ai.thinking);
|
|
270
|
+
return aiSpec(ai.tool, ai.command, ai.caseId, ai.env, ai.sessionId, {
|
|
271
|
+
command: continueCommandLine(ai.tool, ai.command, replyPath, ai.sessionId, ai.model, ai.thinking),
|
|
272
|
+
file,
|
|
273
|
+
args,
|
|
274
|
+
continuesJobId: earlier.info.id,
|
|
275
|
+
}, ai.model, ai.thinking);
|
|
276
|
+
}
|
|
277
|
+
if (request.action === 'make-cases') {
|
|
278
|
+
if (request.memo.trim() === '')
|
|
279
|
+
throw new ActionError('The memo is empty.');
|
|
280
|
+
// The memo is stored as a file so the agent reads it verbatim; the path is generated here, never taken from the browser.
|
|
281
|
+
const memoPath = `${MEMO_DIR}/${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomBytes(3).toString('hex')}.md`;
|
|
282
|
+
fs.mkdirSync(path.join(ROOT, MEMO_DIR), { recursive: true });
|
|
283
|
+
fs.writeFileSync(path.join(ROOT, memoPath), request.memo);
|
|
284
|
+
const { file, args } = makeCaseInvocation(request.tool, memoPath, request.model, request.thinking);
|
|
285
|
+
return {
|
|
286
|
+
kind: 'ai',
|
|
287
|
+
command: makeCaseCommandLine(request.tool, memoPath, request.model, request.thinking),
|
|
288
|
+
file,
|
|
289
|
+
args,
|
|
290
|
+
locks: ['ai:make-cases'],
|
|
291
|
+
acceptsInput: false,
|
|
292
|
+
stream: request.tool,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (!knownEnvs.includes(request.env))
|
|
296
|
+
throw new ActionError(`Unknown environment "${request.env}".`);
|
|
297
|
+
const flags = ['--env', request.env];
|
|
298
|
+
const profilesInEnv = listProfiles(request.env, knownProfiles);
|
|
299
|
+
const profilesAvailable = (request.tags && request.tags.length > 0
|
|
300
|
+
? profilesInEnv.filter((profile) => profile.tags.some((tag) => request.tags.includes(tag)))
|
|
301
|
+
: profilesInEnv).map((profile) => profile.id);
|
|
302
|
+
if (request.profiles) {
|
|
303
|
+
const unknown = request.profiles.filter((id) => !knownProfileIds.includes(id));
|
|
304
|
+
if (unknown.length > 0)
|
|
305
|
+
throw new ActionError(`Unknown profile(s): ${unknown.join(', ')}`);
|
|
306
|
+
const noEnv = request.profiles.filter((id) => !profilesAvailable.includes(id));
|
|
307
|
+
if (noEnv.length > 0)
|
|
308
|
+
throw new ActionError(`No "${request.env}" environment (or tag) for: ${noEnv.join(', ')}`);
|
|
309
|
+
if (request.profiles.length === 0)
|
|
310
|
+
throw new ActionError('Select at least one profile.');
|
|
311
|
+
if (request.profiles.length < profilesAvailable.length) {
|
|
312
|
+
flags.push('--profile', request.profiles.join(','));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (request.tags && request.tags.length > 0)
|
|
316
|
+
flags.push('--tag', request.tags.join(','));
|
|
317
|
+
const suites = request.suites ?? [];
|
|
318
|
+
if (suites.length > 0) {
|
|
319
|
+
const known = listSuites();
|
|
320
|
+
const unknown = suites.filter((name) => !known.includes(name));
|
|
321
|
+
if (unknown.length > 0)
|
|
322
|
+
throw new ActionError(`Unknown suite: ${unknown.join(', ')}`);
|
|
323
|
+
flags.push('--suite', suites.join(','));
|
|
324
|
+
}
|
|
325
|
+
if (request.readOnly)
|
|
326
|
+
flags.push('--read-only');
|
|
327
|
+
const updateBaselines = [...new Set(request.updateBaselines ?? [])];
|
|
328
|
+
if (updateBaselines.length > 0) {
|
|
329
|
+
const known = listCaseIds();
|
|
330
|
+
const unknown = updateBaselines.filter((id) => !isValidCaseId(id) || !known.includes(id) || !fs.existsSync(specFilePath(id)));
|
|
331
|
+
if (unknown.length > 0)
|
|
332
|
+
throw new ActionError(`Cannot update the baselines of: ${unknown.join(', ')} (no such case with a spec).`);
|
|
333
|
+
flags.push('--update-baselines', updateBaselines.join(','));
|
|
334
|
+
}
|
|
335
|
+
// Decided here, from the case documents, not from anything the browser says. The confirmation only counts
|
|
336
|
+
// when it names this exact environment; otherwise the run is refused and the page asks the user.
|
|
337
|
+
if (!request.readOnly && isProtectedEnv(request.env)) {
|
|
338
|
+
const writeCases = findWriteCases(runFilters(request)).writing;
|
|
339
|
+
if (writeCases.length > 0) {
|
|
340
|
+
if (request.confirmWritesIn !== request.env) {
|
|
341
|
+
throw new ActionError(`${writeCases.length} case(s) write data in ${request.env.toUpperCase()}: confirm them, or run read-only.`);
|
|
342
|
+
}
|
|
343
|
+
flags.push(`--allow-writes=${request.env}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (request.grep)
|
|
347
|
+
flags.push(`--grep=${request.grep}`);
|
|
348
|
+
if (request.stepText)
|
|
349
|
+
flags.push('--step-text');
|
|
350
|
+
// The folder of the report is chosen here, so the job can link to it while the run is still going.
|
|
351
|
+
const reportId = `${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}-${request.env}`;
|
|
352
|
+
const displayFlags = flags.map((flag) => (/^[\w@=,.:/-]+$/.test(flag) ? flag : JSON.stringify(flag)));
|
|
353
|
+
return {
|
|
354
|
+
kind: 'run',
|
|
355
|
+
env: request.env,
|
|
356
|
+
command: ['pnpm asrun', ...displayFlags].join(' '),
|
|
357
|
+
file: process.execPath,
|
|
358
|
+
args: [...scriptArgs('run.ts'), ...flags, '--report-id', reportId],
|
|
359
|
+
reportId,
|
|
360
|
+
locks: ['run', ...allLocks()],
|
|
361
|
+
acceptsInput: false,
|
|
362
|
+
...(updateBaselines.length > 0 && { baselineCases: updateBaselines }),
|
|
363
|
+
onFinish: () => void verifyProfiles({ env: request.env }),
|
|
364
|
+
};
|
|
365
|
+
}
|