@asrunjs/asrun 0.0.0-stage → 0.2.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/.claude/commands/explore-case.md +15 -0
- package/.claude/commands/gen-spec.md +13 -0
- package/.claude/commands/heal-spec.md +23 -0
- package/.claude/commands/make-case.md +8 -0
- package/.claude/settings.json +5 -0
- package/.claude/skills/exploring-cases/SKILL.md +132 -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 +112 -0
- package/.claude/skills/exploring-cases/references/readiness.md +137 -0
- package/.claude/skills/exploring-cases/references/user-handoff.md +147 -0
- package/.claude/skills/generating-specs/SKILL.md +207 -0
- package/.claude/skills/generating-specs/references/conventions.md +148 -0
- package/.claude/skills/making-cases/SKILL.md +64 -0
- package/.claude/skills/making-cases/references/gaps.md +36 -0
- package/.claude/skills/making-cases/references/normalizing.md +38 -0
- package/.claude/skills/making-cases/references/splitting.md +28 -0
- package/.claude/skills/making-cases/references/writes-and-email.md +36 -0
- package/AGENTS.md +154 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +410 -3
- 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/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 +29 -0
- package/dist/lib/cases.js +124 -0
- package/dist/lib/cli.d.ts +10 -0
- package/dist/lib/cli.js +18 -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/node-cli.d.ts +11 -0
- package/dist/lib/node-cli.js +26 -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 +38 -0
- package/dist/lib/profiles.js +121 -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 +30 -0
- package/dist/lib/run-summary.d.ts +90 -0
- package/dist/lib/run-summary.js +35 -0
- package/dist/lib/session.d.ts +18 -0
- package/dist/lib/session.js +45 -0
- package/dist/lib/summary-reporter.d.ts +16 -0
- package/dist/lib/summary-reporter.js +146 -0
- package/dist/lib/targets.d.ts +27 -0
- package/dist/lib/targets.js +51 -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 +20 -0
- package/dist/scripts/check-cases.d.ts +1 -0
- package/dist/scripts/check-cases.js +81 -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 +185 -0
- package/dist/scripts/explore.d.ts +1 -0
- package/dist/scripts/explore.js +54 -0
- package/dist/scripts/go.mjs +188 -0
- package/dist/scripts/heal-evidence.mjs +143 -0
- package/dist/scripts/init.d.ts +16 -0
- package/dist/scripts/init.js +284 -0
- package/dist/scripts/lint-specs.d.ts +1 -0
- package/dist/scripts/lint-specs.js +138 -0
- package/dist/scripts/login.d.ts +1 -0
- package/dist/scripts/login.js +47 -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 +91 -0
- package/dist/ui/server/actions.js +352 -0
- package/dist/ui/server/ai-stream.d.ts +41 -0
- package/dist/ui/server/ai-stream.js +202 -0
- package/dist/ui/server/app.d.ts +4 -0
- package/dist/ui/server/app.js +333 -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 +74 -0
- package/dist/ui/server/jobs.js +263 -0
- package/dist/ui/server/profiles.d.ts +16 -0
- package/dist/ui/server/profiles.js +195 -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 +78 -0
- package/dist/ui/shared/ai-commands.js +154 -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 +257 -0
- package/dist/ui/shared/types.js +1 -0
- package/docs/theme-guide.html +692 -0
- package/package.json +98 -3
- package/templates/.claude/commands/explore-case.md +21 -0
- package/templates/.claude/commands/gen-spec.md +20 -0
- package/templates/.claude/commands/heal-spec.md +30 -0
- package/templates/.claude/commands/make-case.md +12 -0
- package/templates/.claude/settings.json +5 -0
- package/templates/.mcp.json +8 -0
- package/templates/AGENTS.md +45 -0
- package/templates/CLAUDE.md +1 -0
- package/templates/cases/_template.md +63 -0
- package/templates/gitignore +13 -0
- package/templates/opencode.jsonc +11 -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 +29 -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-BHaIOZdW.js +1 -0
- package/ui/dist/assets/dist-BSJMM_jp.js +1 -0
- package/ui/dist/assets/dist-BiE4suo9.js +1 -0
- package/ui/dist/assets/dist-BqAROxzI.js +1 -0
- package/ui/dist/assets/dist-Bw5-uxs9.js +1 -0
- package/ui/dist/assets/dist-BwYRbe5s.js +9 -0
- package/ui/dist/assets/dist-BzulKuec.js +1 -0
- package/ui/dist/assets/dist-CBV7_vDN.js +2 -0
- package/ui/dist/assets/dist-CgIQYY1o.js +1 -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-DHCY9KpV.js +1 -0
- package/ui/dist/assets/dist-DX8knLVr.js +1 -0
- package/ui/dist/assets/dist-D_I45xgJ.js +11 -0
- package/ui/dist/assets/dist-Dlorc3Sx.js +1 -0
- package/ui/dist/assets/dist-Dr9crbBz.js +13 -0
- package/ui/dist/assets/dist-gF2bFn-5.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-DmasQ6Rq.css +2 -0
- package/ui/dist/assets/index-I7uU93kk.js +294 -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/index.html +17 -0
- package/ui/dist/logos/claude.svg +1 -0
- package/ui/dist/logos/opencode.svg +1 -0
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
profiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
|
+
hints: z.ZodOptional<z.ZodString>;
|
|
29
|
+
report: z.ZodOptional<z.ZodString>;
|
|
30
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
31
|
+
action: z.ZodLiteral<"continue">;
|
|
32
|
+
jobId: z.ZodString;
|
|
33
|
+
reply: z.ZodString;
|
|
34
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
35
|
+
action: z.ZodLiteral<"make-cases">;
|
|
36
|
+
tool: z.ZodEnum<{
|
|
37
|
+
claude: "claude";
|
|
38
|
+
opencode: "opencode";
|
|
39
|
+
}>;
|
|
40
|
+
memo: z.ZodString;
|
|
41
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
42
|
+
action: z.ZodLiteral<"run">;
|
|
43
|
+
env: z.ZodString;
|
|
44
|
+
profiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
45
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
|
+
suites: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
47
|
+
grep: z.ZodOptional<z.ZodString>;
|
|
48
|
+
stepText: z.ZodOptional<z.ZodBoolean>;
|
|
49
|
+
readOnly: z.ZodOptional<z.ZodBoolean>;
|
|
50
|
+
updateBaselines: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
51
|
+
confirmWritesIn: z.ZodOptional<z.ZodString>;
|
|
52
|
+
}, z.core.$strip>], "action">;
|
|
53
|
+
export declare const previewSchema: z.ZodObject<{
|
|
54
|
+
env: z.ZodString;
|
|
55
|
+
profiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
56
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
57
|
+
suites: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
58
|
+
grep: z.ZodOptional<z.ZodString>;
|
|
59
|
+
readOnly: z.ZodOptional<z.ZodBoolean>;
|
|
60
|
+
}, z.core.$strip>;
|
|
61
|
+
export declare class ActionError extends Error {
|
|
62
|
+
}
|
|
63
|
+
/** The same filters the run will use, so the preview and the real run select the same cases. */
|
|
64
|
+
export declare function runFilters(request: {
|
|
65
|
+
env: string;
|
|
66
|
+
profiles?: string[];
|
|
67
|
+
tags?: string[];
|
|
68
|
+
suites?: string[];
|
|
69
|
+
grep?: string;
|
|
70
|
+
readOnly?: boolean;
|
|
71
|
+
}): {
|
|
72
|
+
env: string;
|
|
73
|
+
profiles: string[] | null;
|
|
74
|
+
suites: string[];
|
|
75
|
+
grep: string | undefined;
|
|
76
|
+
readOnly: boolean;
|
|
77
|
+
};
|
|
78
|
+
/** What a run would do: the cases that write data and whether the user has to confirm them first. */
|
|
79
|
+
export declare function previewRun(request: z.infer<typeof previewSchema>): {
|
|
80
|
+
env: string;
|
|
81
|
+
needsConfirmation: boolean;
|
|
82
|
+
writeCases: import("../../lib/write-gate.js").WriteCase[];
|
|
83
|
+
skippedWriteCases: import("../../lib/write-gate.js").WriteCase[];
|
|
84
|
+
};
|
|
85
|
+
/** The cases a run with these filters would execute, for the Run page. A failure of the list is reported, not thrown. */
|
|
86
|
+
export declare function planRunCases(request: z.infer<typeof previewSchema>): Promise<RunPlan>;
|
|
87
|
+
/**
|
|
88
|
+
* The only way to run something: a fixed list of actions that map to the repo's own scripts.
|
|
89
|
+
* Every value that ends up in argv is checked against profiles.yaml or the cases first.
|
|
90
|
+
*/
|
|
91
|
+
export declare function buildJobSpec(request: z.infer<typeof startJobSchema>): JobSpec;
|
|
@@ -0,0 +1,352 @@
|
|
|
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_MAX_CHARS, } from '../shared/ai-commands.js';
|
|
16
|
+
const VISIT_PATH = '/app/home';
|
|
17
|
+
const REPORTS_DIR = path.join(ROOT, 'reports');
|
|
18
|
+
const REPORT_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-[a-z0-9]+(-[a-z0-9]+)*)?$/;
|
|
19
|
+
const newSessionId = (tool) => tool === 'claude' ? crypto.randomUUID() : `ses_${crypto.randomBytes(12).toString('hex')}`;
|
|
20
|
+
export const startJobSchema = z.discriminatedUnion('action', [
|
|
21
|
+
z.object({ action: z.literal('signin'), profile: z.string().min(1), env: z.string().min(1) }),
|
|
22
|
+
z.object({ action: z.literal('visit'), profile: z.string().min(1), env: z.string().min(1) }),
|
|
23
|
+
z.object({
|
|
24
|
+
action: z.literal('ai'),
|
|
25
|
+
tool: z.enum(AI_TOOLS),
|
|
26
|
+
command: z.enum(AI_COMMANDS),
|
|
27
|
+
caseId: z.string().min(1).max(200),
|
|
28
|
+
env: z.string().min(1),
|
|
29
|
+
force: z.boolean().optional(),
|
|
30
|
+
attended: z.boolean().optional(),
|
|
31
|
+
profiles: z.array(z.string().min(1)).optional(),
|
|
32
|
+
hints: z.string().max(HINTS_MAX_CHARS).optional(),
|
|
33
|
+
report: z.string().min(1).max(100).optional(),
|
|
34
|
+
}),
|
|
35
|
+
z.object({
|
|
36
|
+
action: z.literal('continue'),
|
|
37
|
+
jobId: z.string().min(1).max(40),
|
|
38
|
+
reply: z.string().trim().min(1).max(REPLY_MAX_CHARS),
|
|
39
|
+
}),
|
|
40
|
+
z.object({
|
|
41
|
+
action: z.literal('make-cases'),
|
|
42
|
+
tool: z.enum(AI_TOOLS),
|
|
43
|
+
memo: z.string().max(MEMO_MAX_CHARS),
|
|
44
|
+
}),
|
|
45
|
+
z.object({
|
|
46
|
+
action: z.literal('run'),
|
|
47
|
+
env: z.string().min(1),
|
|
48
|
+
profiles: z.array(z.string().min(1)).optional(),
|
|
49
|
+
tags: z.array(z.string().min(1)).optional(),
|
|
50
|
+
suites: z.array(z.string().min(1)).optional(),
|
|
51
|
+
grep: z.string().max(200).optional(),
|
|
52
|
+
stepText: z.boolean().optional(),
|
|
53
|
+
readOnly: z.boolean().optional(),
|
|
54
|
+
updateBaselines: z.array(z.string().min(1).max(200)).max(200).optional(),
|
|
55
|
+
confirmWritesIn: z.string().optional(),
|
|
56
|
+
}),
|
|
57
|
+
]);
|
|
58
|
+
export const previewSchema = z.object({
|
|
59
|
+
env: z.string().min(1),
|
|
60
|
+
profiles: z.array(z.string().min(1)).optional(),
|
|
61
|
+
tags: z.array(z.string().min(1)).optional(),
|
|
62
|
+
suites: z.array(z.string().min(1)).optional(),
|
|
63
|
+
grep: z.string().max(200).optional(),
|
|
64
|
+
readOnly: z.boolean().optional(),
|
|
65
|
+
});
|
|
66
|
+
export class ActionError extends Error {
|
|
67
|
+
}
|
|
68
|
+
/** The same filters the run will use, so the preview and the real run select the same cases. */
|
|
69
|
+
export function runFilters(request) {
|
|
70
|
+
const known = loadProfiles();
|
|
71
|
+
const knownIds = known.map((profile) => profile.id);
|
|
72
|
+
const profilesInEnv = listProfiles(request.env, known);
|
|
73
|
+
const candidates = request.tags && request.tags.length > 0
|
|
74
|
+
? profilesInEnv.filter((profile) => profile.tags.some((tag) => request.tags.includes(tag)))
|
|
75
|
+
: profilesInEnv;
|
|
76
|
+
const available = candidates.map((profile) => profile.id);
|
|
77
|
+
const selected = request.profiles ?? available;
|
|
78
|
+
if (selected.some((id) => !knownIds.includes(id) || !available.includes(id))) {
|
|
79
|
+
throw new ActionError(`Unknown profile for "${request.env}".`);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
env: request.env,
|
|
83
|
+
profiles: selected.length < available.length ? selected : null,
|
|
84
|
+
suites: request.suites ?? [],
|
|
85
|
+
grep: request.grep,
|
|
86
|
+
readOnly: request.readOnly ?? false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** What a run would do: the cases that write data and whether the user has to confirm them first. */
|
|
90
|
+
export function previewRun(request) {
|
|
91
|
+
if (!listEnvs().includes(request.env))
|
|
92
|
+
throw new ActionError(`Unknown environment "${request.env}".`);
|
|
93
|
+
const { writing, skipped } = request.readOnly ? { writing: [], skipped: [] } : findWriteCases(runFilters(request));
|
|
94
|
+
return {
|
|
95
|
+
env: request.env,
|
|
96
|
+
needsConfirmation: writing.length > 0 && isProtectedEnv(request.env),
|
|
97
|
+
writeCases: writing,
|
|
98
|
+
skippedWriteCases: skipped,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const countBaselines = (caseId) => {
|
|
102
|
+
const dir = snapshotDirPath(caseId);
|
|
103
|
+
return fs.existsSync(dir) ? fs.readdirSync(dir).filter((name) => name.toLowerCase().endsWith('.png')).length : 0;
|
|
104
|
+
};
|
|
105
|
+
/** The cases a run with these filters would execute, for the Run page. A failure of the list is reported, not thrown. */
|
|
106
|
+
export async function planRunCases(request) {
|
|
107
|
+
if (!listEnvs().includes(request.env))
|
|
108
|
+
throw new ActionError(`Unknown environment "${request.env}".`);
|
|
109
|
+
try {
|
|
110
|
+
const cases = (await planRun(runFilters(request))).map((item) => ({ ...item, baselineCount: countBaselines(item.id) }));
|
|
111
|
+
const running = cases.filter((item) => item.skip === null);
|
|
112
|
+
return {
|
|
113
|
+
env: request.env,
|
|
114
|
+
error: null,
|
|
115
|
+
totalCases: running.length,
|
|
116
|
+
totalTests: running.reduce((sum, item) => sum + item.profiles.length, 0),
|
|
117
|
+
cases,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error instanceof ActionError)
|
|
122
|
+
throw error;
|
|
123
|
+
return { env: request.env, error: error.message, totalCases: 0, totalTests: 0, cases: [] };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const authLock = (profileId) => `auth:${profileId}`;
|
|
127
|
+
/**
|
|
128
|
+
* The only way to run something: a fixed list of actions that map to the repo's own scripts.
|
|
129
|
+
* Every value that ends up in argv is checked against profiles.yaml or the cases first.
|
|
130
|
+
*/
|
|
131
|
+
export function buildJobSpec(request) {
|
|
132
|
+
const knownProfiles = loadProfiles();
|
|
133
|
+
const knownProfileIds = knownProfiles.map((profile) => profile.id);
|
|
134
|
+
const knownEnvs = listEnvs(knownProfiles);
|
|
135
|
+
const checkProfile = (profileId, env) => {
|
|
136
|
+
if (!knownProfileIds.includes(profileId))
|
|
137
|
+
throw new ActionError(`Unknown profile "${profileId}".`);
|
|
138
|
+
if (!knownEnvs.includes(env))
|
|
139
|
+
throw new ActionError(`Unknown environment "${env}".`);
|
|
140
|
+
const profile = knownProfiles.find((candidate) => candidate.id === profileId);
|
|
141
|
+
if (profile?.env !== env) {
|
|
142
|
+
throw new ActionError(`Profile "${profileId}" belongs to ${profile?.env ?? 'another environment'}, not "${env}".`);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const allLocks = () => knownProfiles.map((profile) => authLock(profile.id));
|
|
146
|
+
if (request.action === 'signin') {
|
|
147
|
+
checkProfile(request.profile, request.env);
|
|
148
|
+
return {
|
|
149
|
+
kind: 'signin',
|
|
150
|
+
profile: request.profile,
|
|
151
|
+
env: request.env,
|
|
152
|
+
command: `pnpm signin ${request.profile}`,
|
|
153
|
+
file: process.execPath,
|
|
154
|
+
args: [...scriptArgs('login.ts'), request.profile],
|
|
155
|
+
locks: [authLock(request.profile)],
|
|
156
|
+
acceptsInput: true,
|
|
157
|
+
onFinish: () => void verifyProfiles({ ids: [request.profile] }),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (request.action === 'visit') {
|
|
161
|
+
checkProfile(request.profile, request.env);
|
|
162
|
+
// Only reads the saved session when it starts, so it does not take the auth lock
|
|
163
|
+
// (that would block test runs for as long as the window stays open).
|
|
164
|
+
return {
|
|
165
|
+
kind: 'visit',
|
|
166
|
+
profile: request.profile,
|
|
167
|
+
env: request.env,
|
|
168
|
+
command: `pnpm explore ${request.profile} ${VISIT_PATH}`,
|
|
169
|
+
file: process.execPath,
|
|
170
|
+
args: [...scriptArgs('explore.ts'), request.profile, VISIT_PATH],
|
|
171
|
+
locks: [`visit:${request.profile}`],
|
|
172
|
+
acceptsInput: false,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// An AI run on a case, a first turn or a continuation: the same permissions and locks either way.
|
|
176
|
+
// Generating and healing write the spec and the case values and run the tests, so they must not overlap with a run.
|
|
177
|
+
// Reviewing only edits the case file and looks at pages, so it takes just the case lock.
|
|
178
|
+
const aiSpec = (tool, command, caseId, env, sessionId, run) => ({
|
|
179
|
+
kind: 'ai',
|
|
180
|
+
env,
|
|
181
|
+
...run,
|
|
182
|
+
locks: [`ai:${caseId}`, ...(AI_COMMAND_WRITES[command] === 'files' ? ['run', ...allLocks()] : [])],
|
|
183
|
+
acceptsInput: false,
|
|
184
|
+
stream: tool,
|
|
185
|
+
ai: { tool, command, caseId, env, sessionId },
|
|
186
|
+
});
|
|
187
|
+
if (request.action === 'ai') {
|
|
188
|
+
if (!isValidCaseId(request.caseId) || !listCaseIds().includes(request.caseId)) {
|
|
189
|
+
throw new ActionError(`Unknown case "${request.caseId}".`);
|
|
190
|
+
}
|
|
191
|
+
// The environment is part of the prompt, so it is checked like every other value that reaches argv.
|
|
192
|
+
// Every command opens real pages, so there is always one and no default.
|
|
193
|
+
if (!knownEnvs.includes(request.env))
|
|
194
|
+
throw new ActionError(`Unknown environment "${request.env}".`);
|
|
195
|
+
// The server picks the conversation id, so the run can be continued after the agent asked its questions.
|
|
196
|
+
if (request.force && request.command !== 'gen-spec') {
|
|
197
|
+
throw new ActionError('Only gen-spec can overwrite an existing spec.');
|
|
198
|
+
}
|
|
199
|
+
// The agent leaves an existing spec alone unless told to overwrite it, so the user is asked first (the UI shows
|
|
200
|
+
// a dialog); refusing here saves a run that would do nothing.
|
|
201
|
+
if (request.command === 'gen-spec' && !request.force && fs.existsSync(specFilePath(request.caseId))) {
|
|
202
|
+
throw new ActionError(`A spec already exists for "${request.caseId}". Choose overwrite to regenerate it.`);
|
|
203
|
+
}
|
|
204
|
+
if (request.attended && request.command !== 'explore-case') {
|
|
205
|
+
throw new ActionError('Only explore-case can be attended.');
|
|
206
|
+
}
|
|
207
|
+
if ((request.profiles || request.hints !== undefined || request.report) && request.command !== 'heal-spec') {
|
|
208
|
+
throw new ActionError('Only heal-spec takes profiles, hints and a report.');
|
|
209
|
+
}
|
|
210
|
+
const profilesInEnv = listProfiles(request.env, knownProfiles).map((profile) => profile.id);
|
|
211
|
+
if (request.profiles?.some((id) => !profilesInEnv.includes(id))) {
|
|
212
|
+
throw new ActionError(`Unknown profile for "${request.env}".`);
|
|
213
|
+
}
|
|
214
|
+
if (request.profiles?.length === 0)
|
|
215
|
+
throw new ActionError('Choose at least one profile to heal.');
|
|
216
|
+
if (request.report !== undefined && !(REPORT_ID.test(request.report) && fs.existsSync(path.join(REPORTS_DIR, request.report, 'summary.json')))) {
|
|
217
|
+
throw new ActionError(`Unknown report "${request.report}".`);
|
|
218
|
+
}
|
|
219
|
+
// The clues are stored as a file so the agent reads them verbatim; the path is generated here, never taken from the browser.
|
|
220
|
+
let hintsPath;
|
|
221
|
+
if (request.hints !== undefined && request.hints.trim() !== '') {
|
|
222
|
+
hintsPath = `${HINTS_DIR}/${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomBytes(3).toString('hex')}.md`;
|
|
223
|
+
fs.mkdirSync(path.join(ROOT, HINTS_DIR), { recursive: true });
|
|
224
|
+
fs.writeFileSync(path.join(ROOT, hintsPath), request.hints.trim() + '\n');
|
|
225
|
+
}
|
|
226
|
+
const options = {
|
|
227
|
+
env: request.env,
|
|
228
|
+
force: request.force,
|
|
229
|
+
attended: request.attended,
|
|
230
|
+
// Naming every profile is the same as leaving it out: the agent then takes all that are logged in.
|
|
231
|
+
profiles: request.profiles && request.profiles.length < profilesInEnv.length ? request.profiles : undefined,
|
|
232
|
+
hints: hintsPath,
|
|
233
|
+
report: request.report,
|
|
234
|
+
};
|
|
235
|
+
const session = { id: newSessionId(request.tool), resume: false };
|
|
236
|
+
const { file, args } = headlessInvocation(request.tool, request.command, request.caseId, options, session);
|
|
237
|
+
return aiSpec(request.tool, request.command, request.caseId, request.env, session.id, {
|
|
238
|
+
command: headlessCommandLine(request.tool, request.command, request.caseId, options, session),
|
|
239
|
+
file,
|
|
240
|
+
args,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (request.action === 'continue') {
|
|
244
|
+
const earlier = jobs.get(request.jobId);
|
|
245
|
+
const ai = earlier?.spec.ai;
|
|
246
|
+
if (!earlier || !ai)
|
|
247
|
+
throw new ActionError('Unknown job, or it was not an AI run on a case.');
|
|
248
|
+
if (earlier.info.status !== 'succeeded' || earlier.info.resultState !== 'needs-input') {
|
|
249
|
+
throw new ActionError('Only a run that finished with RESULT: needs-input can be continued.');
|
|
250
|
+
}
|
|
251
|
+
if (findContinuation(jobs.list(), earlier.info.id)) {
|
|
252
|
+
throw new ActionError('This run was already continued. Continue the newer run instead.');
|
|
253
|
+
}
|
|
254
|
+
if (!listCaseIds().includes(ai.caseId))
|
|
255
|
+
throw new ActionError(`The case "${ai.caseId}" no longer exists.`);
|
|
256
|
+
const { file, args } = continueInvocation(ai.tool, ai.command, request.reply, ai.sessionId);
|
|
257
|
+
return aiSpec(ai.tool, ai.command, ai.caseId, ai.env, ai.sessionId, {
|
|
258
|
+
command: continueCommandLine(ai.tool, ai.command, request.reply, ai.sessionId),
|
|
259
|
+
file,
|
|
260
|
+
args,
|
|
261
|
+
continuesJobId: earlier.info.id,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (request.action === 'make-cases') {
|
|
265
|
+
if (request.memo.trim() === '')
|
|
266
|
+
throw new ActionError('The memo is empty.');
|
|
267
|
+
// The memo is stored as a file so the agent reads it verbatim; the path is generated here, never taken from the browser.
|
|
268
|
+
const memoPath = `${MEMO_DIR}/${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomBytes(3).toString('hex')}.md`;
|
|
269
|
+
fs.mkdirSync(path.join(ROOT, MEMO_DIR), { recursive: true });
|
|
270
|
+
fs.writeFileSync(path.join(ROOT, memoPath), request.memo);
|
|
271
|
+
const { file, args } = makeCaseInvocation(request.tool, memoPath);
|
|
272
|
+
return {
|
|
273
|
+
kind: 'ai',
|
|
274
|
+
command: makeCaseCommandLine(request.tool, memoPath),
|
|
275
|
+
file,
|
|
276
|
+
args,
|
|
277
|
+
locks: ['ai:make-cases'],
|
|
278
|
+
acceptsInput: false,
|
|
279
|
+
stream: request.tool,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (!knownEnvs.includes(request.env))
|
|
283
|
+
throw new ActionError(`Unknown environment "${request.env}".`);
|
|
284
|
+
const flags = ['--env', request.env];
|
|
285
|
+
const profilesInEnv = listProfiles(request.env, knownProfiles);
|
|
286
|
+
const profilesAvailable = (request.tags && request.tags.length > 0
|
|
287
|
+
? profilesInEnv.filter((profile) => profile.tags.some((tag) => request.tags.includes(tag)))
|
|
288
|
+
: profilesInEnv).map((profile) => profile.id);
|
|
289
|
+
if (request.profiles) {
|
|
290
|
+
const unknown = request.profiles.filter((id) => !knownProfileIds.includes(id));
|
|
291
|
+
if (unknown.length > 0)
|
|
292
|
+
throw new ActionError(`Unknown profile(s): ${unknown.join(', ')}`);
|
|
293
|
+
const noEnv = request.profiles.filter((id) => !profilesAvailable.includes(id));
|
|
294
|
+
if (noEnv.length > 0)
|
|
295
|
+
throw new ActionError(`No "${request.env}" environment (or tag) for: ${noEnv.join(', ')}`);
|
|
296
|
+
if (request.profiles.length === 0)
|
|
297
|
+
throw new ActionError('Select at least one profile.');
|
|
298
|
+
if (request.profiles.length < profilesAvailable.length) {
|
|
299
|
+
flags.push('--profile', request.profiles.join(','));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (request.tags && request.tags.length > 0)
|
|
303
|
+
flags.push('--tag', request.tags.join(','));
|
|
304
|
+
const suites = request.suites ?? [];
|
|
305
|
+
if (suites.length > 0) {
|
|
306
|
+
const known = listSuites();
|
|
307
|
+
const unknown = suites.filter((name) => !known.includes(name));
|
|
308
|
+
if (unknown.length > 0)
|
|
309
|
+
throw new ActionError(`Unknown suite: ${unknown.join(', ')}`);
|
|
310
|
+
flags.push('--suite', suites.join(','));
|
|
311
|
+
}
|
|
312
|
+
if (request.readOnly)
|
|
313
|
+
flags.push('--read-only');
|
|
314
|
+
const updateBaselines = [...new Set(request.updateBaselines ?? [])];
|
|
315
|
+
if (updateBaselines.length > 0) {
|
|
316
|
+
const known = listCaseIds();
|
|
317
|
+
const unknown = updateBaselines.filter((id) => !isValidCaseId(id) || !known.includes(id) || !fs.existsSync(specFilePath(id)));
|
|
318
|
+
if (unknown.length > 0)
|
|
319
|
+
throw new ActionError(`Cannot update the baselines of: ${unknown.join(', ')} (no such case with a spec).`);
|
|
320
|
+
flags.push('--update-baselines', updateBaselines.join(','));
|
|
321
|
+
}
|
|
322
|
+
// Decided here, from the case documents, not from anything the browser says. The confirmation only counts
|
|
323
|
+
// when it names this exact environment; otherwise the run is refused and the page asks the user.
|
|
324
|
+
if (!request.readOnly && isProtectedEnv(request.env)) {
|
|
325
|
+
const writeCases = findWriteCases(runFilters(request)).writing;
|
|
326
|
+
if (writeCases.length > 0) {
|
|
327
|
+
if (request.confirmWritesIn !== request.env) {
|
|
328
|
+
throw new ActionError(`${writeCases.length} case(s) write data in ${request.env.toUpperCase()}: confirm them, or run read-only.`);
|
|
329
|
+
}
|
|
330
|
+
flags.push(`--allow-writes=${request.env}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (request.grep)
|
|
334
|
+
flags.push(`--grep=${request.grep}`);
|
|
335
|
+
if (request.stepText)
|
|
336
|
+
flags.push('--step-text');
|
|
337
|
+
// The folder of the report is chosen here, so the job can link to it while the run is still going.
|
|
338
|
+
const reportId = `${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}-${request.env}`;
|
|
339
|
+
const displayFlags = flags.map((flag) => (/^[\w@=,.:/-]+$/.test(flag) ? flag : JSON.stringify(flag)));
|
|
340
|
+
return {
|
|
341
|
+
kind: 'run',
|
|
342
|
+
env: request.env,
|
|
343
|
+
command: ['pnpm asrun', ...displayFlags].join(' '),
|
|
344
|
+
file: process.execPath,
|
|
345
|
+
args: [...scriptArgs('run.ts'), ...flags, '--report-id', reportId],
|
|
346
|
+
reportId,
|
|
347
|
+
locks: ['run', ...allLocks()],
|
|
348
|
+
acceptsInput: false,
|
|
349
|
+
...(updateBaselines.length > 0 && { baselineCases: updateBaselines }),
|
|
350
|
+
onFinish: () => void verifyProfiles({ env: request.env }),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AiTool } from '../shared/ai-commands.js';
|
|
2
|
+
/** The agent has put a handoff card in the browser window and waits for the person (see user-handoff.md). */
|
|
3
|
+
export interface AwaitingUser {
|
|
4
|
+
message: string | null;
|
|
5
|
+
since: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class AiStreamFormatter {
|
|
8
|
+
private readonly tool;
|
|
9
|
+
private buffer;
|
|
10
|
+
private pending;
|
|
11
|
+
private cardCalls;
|
|
12
|
+
private waitingForUser;
|
|
13
|
+
private lastEventAt;
|
|
14
|
+
private lastWaitAt;
|
|
15
|
+
private sandboxHinted;
|
|
16
|
+
constructor(tool: AiTool);
|
|
17
|
+
/** Lines for a piece of stdout. Incomplete trailing lines wait for the next piece. */
|
|
18
|
+
feed(text: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Set while a handoff card waits for the person. OpenCode reports a tool call only when it has finished, so the card
|
|
21
|
+
* call itself is not visible while it waits: the options call before it (`__handoffCfg`) starts the state and the end
|
|
22
|
+
* of the card call, or any other call, ends it. The object changes on every new handoff, so `===` detects a change.
|
|
23
|
+
*/
|
|
24
|
+
get awaiting(): AwaitingUser | null;
|
|
25
|
+
/** Called once for every tool call the agent makes, with the id the end of that call will carry. */
|
|
26
|
+
private watchHandoff;
|
|
27
|
+
/** What is left when the process ends. */
|
|
28
|
+
end(): string;
|
|
29
|
+
/**
|
|
30
|
+
* A line about the oldest tool call that has not returned, when nothing has happened for `quietMs` (and again every
|
|
31
|
+
* `quietMs` after that).
|
|
32
|
+
*/
|
|
33
|
+
waiting(quietMs: number): string;
|
|
34
|
+
private line;
|
|
35
|
+
private start;
|
|
36
|
+
private end1;
|
|
37
|
+
/** Said once per run, after the first tool call that Claude Code's sandbox refused (nobody is there to approve it). */
|
|
38
|
+
private sandboxHint;
|
|
39
|
+
private claude;
|
|
40
|
+
private opencode;
|
|
41
|
+
}
|