@loadmill/droid-cua 2.23.0 → 2.24.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/README.md +25 -0
- package/build/index.js +12 -0
- package/build/src/cli/skill-command.js +240 -0
- package/build/src/commands/help.js +2 -1
- package/build/src/core/cua-models.js +20 -9
- package/build/src/core/droid-cua-run-usage.js +16 -7
- package/build/src/core/runtime-config.js +2 -5
- package/package.json +2 -1
- package/skill/droid-cua/SKILL.md +36 -0
- package/skill/droid-cua/agents/openai.yaml +6 -0
- package/skill/droid-cua/assets/droid-cua-app-icon.png +0 -0
- package/skill/droid-cua/references/authoring-tests.md +39 -0
- package/skill/droid-cua/references/onboarding-and-troubleshooting.md +74 -0
- package/skill/droid-cua/references/project-and-auth.md +38 -0
- package/skill/droid-cua/references/running-and-healing.md +70 -0
package/README.md
CHANGED
|
@@ -134,6 +134,27 @@ Install:
|
|
|
134
134
|
npm install -g @loadmill/droid-cua
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
Install the bundled Agent Skill for your coding agent:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
# Global installation (default)
|
|
141
|
+
droid-cua skill install cursor
|
|
142
|
+
droid-cua skill install claude-code
|
|
143
|
+
droid-cua skill install codex
|
|
144
|
+
|
|
145
|
+
# Or install in the current project
|
|
146
|
+
droid-cua skill install codex --local
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Use `droid-cua skill status <agent>` to inspect an installation and
|
|
150
|
+
`droid-cua skill uninstall <agent>` to remove it. Installed skill
|
|
151
|
+
files are never overwritten or removed after local modification unless `--force`
|
|
152
|
+
is supplied. After upgrading the npm package, update an installed skill explicitly:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
droid-cua skill install <agent> --force
|
|
156
|
+
```
|
|
157
|
+
|
|
137
158
|
Examples:
|
|
138
159
|
```sh
|
|
139
160
|
# Interactive CLI
|
|
@@ -230,6 +251,10 @@ Supported CLI options include:
|
|
|
230
251
|
- `--report`
|
|
231
252
|
- `--debug`
|
|
232
253
|
|
|
254
|
+
`--cua-model` accepts `loadmill-smart`, `loadmill-beacon`, `gpt-5.6-terra`,
|
|
255
|
+
`gpt-5.6-luna`, and `gpt-5.4`. OpenAI models use direct OpenAI mode and require
|
|
256
|
+
`OPENAI_API_KEY`.
|
|
257
|
+
|
|
233
258
|
Config and precedence rules:
|
|
234
259
|
- Use `--config <file>` to supply prompt-affecting settings for headless runs.
|
|
235
260
|
- CLI flags override config file values.
|
package/build/index.js
CHANGED
|
@@ -29,8 +29,20 @@ import { createStatusLine } from "./src/cli/cloud-status-line.js";
|
|
|
29
29
|
import { createLoadmillApiClient } from "./src/integrations/loadmill/client.js";
|
|
30
30
|
import { getCloudProviderAdapter } from "./src/device/cloud/registry.js";
|
|
31
31
|
import { setCloudProgressListener } from "./src/device/cloud/progress.js";
|
|
32
|
+
import { runSkillCommand } from "./src/cli/skill-command.js";
|
|
32
33
|
dotenv.config();
|
|
33
34
|
const args = minimist(process.argv.slice(2));
|
|
35
|
+
if (args._[0] === "skill") {
|
|
36
|
+
try {
|
|
37
|
+
await runSkillCommand({ args });
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
console.error(`\nSkill command failed: ${message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
34
46
|
const targetConfig = resolveHeadlessTarget(args);
|
|
35
47
|
let avdName = targetConfig.avdName;
|
|
36
48
|
let platform = targetConfig.platform || null; // 'ios' or 'android'
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { cp, lstat, mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
5
|
+
const SKILL_NAME = "droid-cua";
|
|
6
|
+
const AGENTS = Object.freeze({
|
|
7
|
+
cursor: { displayName: "Cursor", relativePath: path.join(".cursor", "skills", SKILL_NAME) },
|
|
8
|
+
"claude-code": { displayName: "Claude Code", relativePath: path.join(".claude", "skills", SKILL_NAME) },
|
|
9
|
+
codex: { displayName: "Codex", relativePath: path.join(".agents", "skills", SKILL_NAME) },
|
|
10
|
+
});
|
|
11
|
+
const SUBCOMMANDS = new Set(["install", "status", "uninstall"]);
|
|
12
|
+
const ALLOWED_OPTIONS = new Set(["_", "local", "force", "h", "help"]);
|
|
13
|
+
const COMMAND_DESCRIPTIONS = Object.freeze({
|
|
14
|
+
install: "Install Droid CUA skill for a coding agent",
|
|
15
|
+
status: "Show Droid CUA skill status for a coding agent",
|
|
16
|
+
uninstall: "Uninstall Droid CUA skill for a coding agent",
|
|
17
|
+
});
|
|
18
|
+
function skillCommandHelp(subcommand) {
|
|
19
|
+
const localDescriptions = {
|
|
20
|
+
install: "Install in current project instead of global",
|
|
21
|
+
status: "Check current project instead of global",
|
|
22
|
+
uninstall: "Uninstall from current project instead of global",
|
|
23
|
+
};
|
|
24
|
+
const options = [
|
|
25
|
+
` --local ${localDescriptions[subcommand]}`,
|
|
26
|
+
];
|
|
27
|
+
if (subcommand !== "status") {
|
|
28
|
+
options.push(" --force Overwrite or remove modified skill files");
|
|
29
|
+
}
|
|
30
|
+
options.push(" -h, --help display help for command");
|
|
31
|
+
return [
|
|
32
|
+
`Usage: droid-cua skill ${subcommand} [options] [agent]`,
|
|
33
|
+
"",
|
|
34
|
+
COMMAND_DESCRIPTIONS[subcommand],
|
|
35
|
+
"",
|
|
36
|
+
"Agents:",
|
|
37
|
+
" cursor",
|
|
38
|
+
" claude-code",
|
|
39
|
+
" codex",
|
|
40
|
+
"",
|
|
41
|
+
"Example:",
|
|
42
|
+
` droid-cua skill ${subcommand} claude-code`,
|
|
43
|
+
"",
|
|
44
|
+
"Options:",
|
|
45
|
+
...options,
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
function skillRootHelp() {
|
|
49
|
+
return [
|
|
50
|
+
"Usage: droid-cua skill <command> [options] [agent]",
|
|
51
|
+
"",
|
|
52
|
+
"Manage the bundled Droid CUA skill",
|
|
53
|
+
"",
|
|
54
|
+
"Commands:",
|
|
55
|
+
" install Install the skill",
|
|
56
|
+
" status Show installation status",
|
|
57
|
+
" uninstall Uninstall the skill",
|
|
58
|
+
].join("\n");
|
|
59
|
+
}
|
|
60
|
+
async function pathType(targetPath) {
|
|
61
|
+
try {
|
|
62
|
+
const stats = await lstat(targetPath);
|
|
63
|
+
if (stats.isDirectory())
|
|
64
|
+
return "directory";
|
|
65
|
+
if (stats.isFile())
|
|
66
|
+
return "file";
|
|
67
|
+
if (stats.isSymbolicLink())
|
|
68
|
+
return "symlink";
|
|
69
|
+
return "other";
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error?.code === "ENOENT")
|
|
73
|
+
return "missing";
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function snapshotDirectory(rootPath, relativePath = "") {
|
|
78
|
+
const absolutePath = relativePath ? path.join(rootPath, relativePath) : rootPath;
|
|
79
|
+
const entries = await readdir(absolutePath, { withFileTypes: true });
|
|
80
|
+
const snapshot = [];
|
|
81
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
82
|
+
const childRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
snapshot.push({ path: `${childRelativePath}/`, type: "directory" });
|
|
85
|
+
snapshot.push(...await snapshotDirectory(rootPath, childRelativePath));
|
|
86
|
+
}
|
|
87
|
+
else if (entry.isFile()) {
|
|
88
|
+
const contents = await readFile(path.join(rootPath, childRelativePath));
|
|
89
|
+
snapshot.push({ path: childRelativePath, type: "file", contents: contents.toString("base64") });
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
snapshot.push({ path: childRelativePath, type: entry.isSymbolicLink() ? "symlink" : "other" });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return snapshot;
|
|
96
|
+
}
|
|
97
|
+
async function directoryMatches(sourcePath, destinationPath) {
|
|
98
|
+
if (await pathType(destinationPath) !== "directory")
|
|
99
|
+
return false;
|
|
100
|
+
const [sourceSnapshot, destinationSnapshot] = await Promise.all([
|
|
101
|
+
snapshotDirectory(sourcePath),
|
|
102
|
+
snapshotDirectory(destinationPath),
|
|
103
|
+
]);
|
|
104
|
+
return JSON.stringify(sourceSnapshot) === JSON.stringify(destinationSnapshot);
|
|
105
|
+
}
|
|
106
|
+
async function getInstallationState(sourcePath, destinationPath) {
|
|
107
|
+
if (await pathType(destinationPath) === "missing")
|
|
108
|
+
return "not-installed";
|
|
109
|
+
return await directoryMatches(sourcePath, destinationPath) ? "installed" : "modified";
|
|
110
|
+
}
|
|
111
|
+
function normalizeStringOption(value, label) {
|
|
112
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
113
|
+
throw new Error(`${label} requires a value.`);
|
|
114
|
+
}
|
|
115
|
+
return value.trim().toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
function parseSkillCommandArgs(args) {
|
|
118
|
+
const unknownOption = Object.keys(args).find((key) => !ALLOWED_OPTIONS.has(key));
|
|
119
|
+
if (unknownOption)
|
|
120
|
+
throw new Error(`Unknown skill option --${unknownOption}.`);
|
|
121
|
+
if (!Array.isArray(args._) || args._[0] !== "skill") {
|
|
122
|
+
throw new Error("Usage: droid-cua skill <command> [options] [agent]");
|
|
123
|
+
}
|
|
124
|
+
if (args._.length === 1) {
|
|
125
|
+
return { help: skillRootHelp() };
|
|
126
|
+
}
|
|
127
|
+
const subcommand = normalizeStringOption(args._[1], "Skill subcommand");
|
|
128
|
+
if (!SUBCOMMANDS.has(subcommand)) {
|
|
129
|
+
throw new Error(`Unknown skill subcommand "${subcommand}". Expected install, status, or uninstall.`);
|
|
130
|
+
}
|
|
131
|
+
for (const option of ["local", "force", "h", "help"]) {
|
|
132
|
+
if (args[option] != null && typeof args[option] !== "boolean") {
|
|
133
|
+
throw new Error(`--${option === "h" ? "help" : option} does not accept a value.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (args.help === true || args.h === true || args._.length === 2) {
|
|
137
|
+
return { help: skillCommandHelp(subcommand) };
|
|
138
|
+
}
|
|
139
|
+
if (args._.length !== 3)
|
|
140
|
+
throw new Error(skillCommandHelp(subcommand));
|
|
141
|
+
if (subcommand === "status" && args.force != null) {
|
|
142
|
+
throw new Error("--force is only supported by install and uninstall.");
|
|
143
|
+
}
|
|
144
|
+
const agent = normalizeStringOption(args._[2], "Agent");
|
|
145
|
+
if (!Object.hasOwn(AGENTS, agent)) {
|
|
146
|
+
throw new Error(`Unknown agent "${agent}". Expected cursor, claude-code, or codex.`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
subcommand,
|
|
150
|
+
agent,
|
|
151
|
+
local: args.local === true,
|
|
152
|
+
force: args.force === true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function resolveSkillDestination({ agent, local = false, cwd = process.cwd(), home = os.homedir() }) {
|
|
156
|
+
const basePath = local ? cwd : home;
|
|
157
|
+
return {
|
|
158
|
+
agent,
|
|
159
|
+
...AGENTS[agent],
|
|
160
|
+
path: path.join(basePath, AGENTS[agent].relativePath),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export async function resolveBundledSkillPath({ startDir = path.dirname(fileURLToPath(import.meta.url)) } = {}) {
|
|
164
|
+
let currentPath = path.resolve(startDir);
|
|
165
|
+
while (true) {
|
|
166
|
+
const candidate = path.join(currentPath, "skill", SKILL_NAME);
|
|
167
|
+
if (await pathType(path.join(candidate, "SKILL.md")) === "file")
|
|
168
|
+
return candidate;
|
|
169
|
+
const parentPath = path.dirname(currentPath);
|
|
170
|
+
if (parentPath === currentPath)
|
|
171
|
+
break;
|
|
172
|
+
currentPath = parentPath;
|
|
173
|
+
}
|
|
174
|
+
throw new Error("Bundled Droid CUA skill was not found. Reinstall @loadmill/droid-cua and try again.");
|
|
175
|
+
}
|
|
176
|
+
function stateLabel(state) {
|
|
177
|
+
if (state === "not-installed")
|
|
178
|
+
return "not installed";
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
181
|
+
function shouldUseColor() {
|
|
182
|
+
return Boolean(process.stdout.isTTY) && !Object.hasOwn(process.env, "NO_COLOR");
|
|
183
|
+
}
|
|
184
|
+
function formatInstallSuccess(destination, { alreadyInstalled, color }) {
|
|
185
|
+
const checkmark = color ? "\u001b[32m✓\u001b[39m" : "✓";
|
|
186
|
+
const skillFilePath = path.join(destination.path, "SKILL.md");
|
|
187
|
+
const displayedPath = color ? `\u001b[2m${skillFilePath}\u001b[22m` : skillFilePath;
|
|
188
|
+
const message = alreadyInstalled
|
|
189
|
+
? `${destination.agent} skill already installed`
|
|
190
|
+
: `Installed ${destination.agent} skill`;
|
|
191
|
+
return `${checkmark} ${message}\n${displayedPath}`;
|
|
192
|
+
}
|
|
193
|
+
export async function runSkillCommand({ args, cwd = process.cwd(), home = os.homedir(), sourcePath = null, output = console, color = shouldUseColor(), } = {}) {
|
|
194
|
+
const command = parseSkillCommandArgs(args ?? {});
|
|
195
|
+
if (command.help) {
|
|
196
|
+
output.log(command.help);
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
const bundledSkillPath = sourcePath ?? await resolveBundledSkillPath();
|
|
200
|
+
if (await pathType(path.join(bundledSkillPath, "SKILL.md")) !== "file") {
|
|
201
|
+
throw new Error(`Invalid bundled skill at ${bundledSkillPath}: SKILL.md is missing.`);
|
|
202
|
+
}
|
|
203
|
+
const destination = resolveSkillDestination({ agent: command.agent, local: command.local, cwd, home });
|
|
204
|
+
const states = [{
|
|
205
|
+
...destination,
|
|
206
|
+
state: await getInstallationState(bundledSkillPath, destination.path),
|
|
207
|
+
}];
|
|
208
|
+
if (command.subcommand !== "status" && !command.force) {
|
|
209
|
+
const conflicts = states.filter(({ state }) => state === "modified");
|
|
210
|
+
if (conflicts.length > 0) {
|
|
211
|
+
const paths = conflicts.map(({ path: destinationPath }) => destinationPath).join(", ");
|
|
212
|
+
throw new Error(`Refusing to ${command.subcommand} modified skill files at ${paths}. Re-run with --force to replace them.`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const destination of states) {
|
|
216
|
+
if (command.subcommand === "status") {
|
|
217
|
+
output.log(`${destination.displayName}: ${stateLabel(destination.state)} at ${destination.path}`);
|
|
218
|
+
}
|
|
219
|
+
else if (command.subcommand === "install") {
|
|
220
|
+
if (destination.state === "installed") {
|
|
221
|
+
output.log(formatInstallSuccess(destination, { alreadyInstalled: true, color }));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (destination.state !== "not-installed")
|
|
225
|
+
await rm(destination.path, { recursive: true, force: true });
|
|
226
|
+
await mkdir(path.dirname(destination.path), { recursive: true });
|
|
227
|
+
await cp(bundledSkillPath, destination.path, { recursive: true });
|
|
228
|
+
output.log(formatInstallSuccess(destination, { alreadyInstalled: false, color }));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (destination.state === "not-installed") {
|
|
232
|
+
output.log(`${destination.displayName}: not installed at ${destination.path}`);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
await rm(destination.path, { recursive: true, force: true });
|
|
236
|
+
output.log(`${destination.displayName}: uninstalled from ${destination.path}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return states.map(({ agent, path: destinationPath, state }) => ({ agent, path: destinationPath, state }));
|
|
240
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Help command handler
|
|
3
3
|
*/
|
|
4
|
+
import { CUA_MODELS } from '../core/cua-models.js';
|
|
4
5
|
/**
|
|
5
6
|
* Handle /help command
|
|
6
7
|
* @param {string} args - Command arguments
|
|
@@ -32,7 +33,7 @@ export async function handleHelp(args, session, context) {
|
|
|
32
33
|
addOutput({ type: 'info', text: ' --session-mode <mode> Browser session mode for web runs: persistent or fresh' });
|
|
33
34
|
addOutput({ type: 'info', text: ' --config <file> Headless execution JSON config for prompt parity' });
|
|
34
35
|
addOutput({ type: 'info', text: ' --llm-provider <mode> Headless AI provider: loadmill or openai' });
|
|
35
|
-
addOutput({ type: 'info', text:
|
|
36
|
+
addOutput({ type: 'info', text: ` --cua-model <model> Headless CUA model override: ${CUA_MODELS.join(', ')}` });
|
|
36
37
|
addOutput({ type: 'info', text: ' --aim-assist <mode> Headless Aim Assist mode: off, smart, or always' });
|
|
37
38
|
addOutput({ type: 'info', text: ' --context <file> Optional app context file injected into execution runs' });
|
|
38
39
|
addOutput({ type: 'info', text: ' --no-context Disable app context for headless execution' });
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
export const DEFAULT_OPENAI_CUA_MODEL = "gpt-5.4";
|
|
2
2
|
export const DEFAULT_LOADMILL_CUA_MODEL = "loadmill-smart";
|
|
3
3
|
export const DEFAULT_CUA_MODEL = DEFAULT_LOADMILL_CUA_MODEL;
|
|
4
|
-
export const OPENAI_CUA_MODELS = ["gpt-5.4", "gpt-5.4-mini"];
|
|
5
|
-
export const LOADMILL_CUA_MODELS = [DEFAULT_LOADMILL_CUA_MODEL, "loadmill-beacon"];
|
|
6
|
-
export const CUA_MODELS = ["gpt-5.4", "loadmill-smart", "loadmill-beacon", "gpt-5.4-mini"];
|
|
7
4
|
export const CUA_MODEL_OPTIONS = Object.freeze([
|
|
8
5
|
Object.freeze({
|
|
9
6
|
id: "loadmill-smart",
|
|
@@ -20,20 +17,34 @@ export const CUA_MODEL_OPTIONS = Object.freeze([
|
|
|
20
17
|
label: "Loadmill - Beacon",
|
|
21
18
|
}),
|
|
22
19
|
Object.freeze({
|
|
23
|
-
id: "gpt-5.
|
|
20
|
+
id: "gpt-5.6-terra",
|
|
24
21
|
providerMode: "openai",
|
|
25
22
|
providerLabel: "OpenAI",
|
|
26
|
-
name: "GPT-5.
|
|
27
|
-
label: "OpenAI GPT-5.
|
|
23
|
+
name: "GPT-5.6 Terra",
|
|
24
|
+
label: "OpenAI GPT-5.6 Terra",
|
|
25
|
+
}),
|
|
26
|
+
Object.freeze({
|
|
27
|
+
id: "gpt-5.6-luna",
|
|
28
|
+
providerMode: "openai",
|
|
29
|
+
providerLabel: "OpenAI",
|
|
30
|
+
name: "GPT-5.6 Luna",
|
|
31
|
+
label: "OpenAI GPT-5.6 Luna",
|
|
28
32
|
}),
|
|
29
33
|
Object.freeze({
|
|
30
|
-
id: "gpt-5.4
|
|
34
|
+
id: "gpt-5.4",
|
|
31
35
|
providerMode: "openai",
|
|
32
36
|
providerLabel: "OpenAI",
|
|
33
|
-
name: "GPT-5.4
|
|
34
|
-
label: "OpenAI GPT-5.4
|
|
37
|
+
name: "GPT-5.4",
|
|
38
|
+
label: "OpenAI GPT-5.4",
|
|
35
39
|
}),
|
|
36
40
|
]);
|
|
41
|
+
export const CUA_MODELS = Object.freeze(CUA_MODEL_OPTIONS.map((option) => option.id));
|
|
42
|
+
export const OPENAI_CUA_MODELS = Object.freeze(CUA_MODEL_OPTIONS
|
|
43
|
+
.filter((option) => option.providerMode === "openai")
|
|
44
|
+
.map((option) => option.id));
|
|
45
|
+
export const LOADMILL_CUA_MODELS = Object.freeze(CUA_MODEL_OPTIONS
|
|
46
|
+
.filter((option) => option.providerMode === "loadmill")
|
|
47
|
+
.map((option) => option.id));
|
|
37
48
|
const VALID_CUA_MODELS = new Set(CUA_MODELS);
|
|
38
49
|
const OPENAI_CUA_MODEL_SET = new Set(OPENAI_CUA_MODELS);
|
|
39
50
|
const LOADMILL_CUA_MODEL_SET = new Set(LOADMILL_CUA_MODELS);
|
|
@@ -2,16 +2,22 @@ const TOKENS_PER_MILLION = 1_000_000;
|
|
|
2
2
|
const OPENAI_GPT_5_4_PRICING = Object.freeze({
|
|
3
3
|
inputPerMillionUsd: 2.5,
|
|
4
4
|
});
|
|
5
|
-
const
|
|
6
|
-
inputPerMillionUsd:
|
|
5
|
+
const OPENAI_GPT_5_6_TERRA_PRICING = Object.freeze({
|
|
6
|
+
inputPerMillionUsd: 2.5,
|
|
7
|
+
});
|
|
8
|
+
const OPENAI_GPT_5_6_LUNA_PRICING = Object.freeze({
|
|
9
|
+
inputPerMillionUsd: 1,
|
|
7
10
|
});
|
|
8
11
|
const MODEL_PRICING = Object.freeze({
|
|
12
|
+
"gpt-5.6-terra": Object.freeze({
|
|
13
|
+
...OPENAI_GPT_5_6_TERRA_PRICING,
|
|
14
|
+
}),
|
|
15
|
+
"gpt-5.6-luna": Object.freeze({
|
|
16
|
+
...OPENAI_GPT_5_6_LUNA_PRICING,
|
|
17
|
+
}),
|
|
9
18
|
"gpt-5.4": Object.freeze({
|
|
10
19
|
...OPENAI_GPT_5_4_PRICING,
|
|
11
20
|
}),
|
|
12
|
-
"gpt-5.4-mini": Object.freeze({
|
|
13
|
-
...OPENAI_GPT_5_4_MINI_PRICING,
|
|
14
|
-
}),
|
|
15
21
|
// Loadmill Smart currently routes to GPT-5.4 behind the Loadmill LLM gateway.
|
|
16
22
|
// Keep this explicit so pricing can be updated here if the backing model changes.
|
|
17
23
|
"loadmill-smart": Object.freeze({
|
|
@@ -32,8 +38,11 @@ export function normalizeDroidCuaCostModel(model) {
|
|
|
32
38
|
if (typeof model !== "string") {
|
|
33
39
|
return null;
|
|
34
40
|
}
|
|
35
|
-
if (model === "gpt-5.
|
|
36
|
-
return "gpt-5.
|
|
41
|
+
if (model === "gpt-5.6-terra" || model.startsWith("gpt-5.6-terra-")) {
|
|
42
|
+
return "gpt-5.6-terra";
|
|
43
|
+
}
|
|
44
|
+
if (model === "gpt-5.6-luna" || model.startsWith("gpt-5.6-luna-")) {
|
|
45
|
+
return "gpt-5.6-luna";
|
|
37
46
|
}
|
|
38
47
|
if (model === "gpt-5.4" || model.startsWith("gpt-5.4-")) {
|
|
39
48
|
return "gpt-5.4";
|
|
@@ -90,11 +90,8 @@ export function inferLlmProviderModeFromCredentials(credentials = {}) {
|
|
|
90
90
|
}
|
|
91
91
|
export function resolveGatewayCuaModelAlias({ cuaModel } = {}) {
|
|
92
92
|
const normalizedCuaModel = resolveCuaModel(cuaModel);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
return normalizedCuaModel === "gpt-5.4-mini"
|
|
97
|
-
? LOADMILL_MODEL_ALIASES.FAST
|
|
93
|
+
return normalizedCuaModel === "loadmill-beacon"
|
|
94
|
+
? LOADMILL_MODEL_ALIASES.BEACON
|
|
98
95
|
: LOADMILL_MODEL_ALIASES.SMART;
|
|
99
96
|
}
|
|
100
97
|
export function resolveEffectiveCuaModel({ llmProviderMode = null, cuaModel = null, } = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadmill/droid-cua",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.24.0",
|
|
4
4
|
"description": "AI-powered Android testing agent using OpenAI's computer-use model and ADB",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"build/**/*",
|
|
12
12
|
"bin/**/*",
|
|
13
|
+
"skill/**/*",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: droid-cua
|
|
3
|
+
description: Write, run, monitor, and troubleshoot Droid CUA mobile and web tests. Use when an agent needs to onboard @loadmill/droid-cua, configure Loadmill authentication, discover Android devices or AVDs, prepare iOS simulators or local browsers, author or revise .dcua tests and project context, run tests locally or on supported cloud devices, analyze reports and debug artifacts, distinguish test failures from product bugs, or perform guarded test healing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Droid CUA
|
|
7
|
+
|
|
8
|
+
Use the existing `droid-cua` CLI and repository files. Do not invent commands, flags, or product capabilities.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Inspect the active test project before creating files. Find existing `.dcua` tests, `context.md`, `test-data.md`, `.secrets`, `.env`, and `ci/droid-cua.json` without reading secret values. Do not search other projects or the user's home directory for credentials.
|
|
13
|
+
2. Determine the requested platform and target from the task or project. Ask the user only when the choice is unclear.
|
|
14
|
+
3. Check authentication and environment readiness before the first run. Read [project-and-auth.md](references/project-and-auth.md) for project boundaries and token handling. Read [onboarding-and-troubleshooting.md](references/onboarding-and-troubleshooting.md) when setup, dependency, device, simulator, browser, or connection work is needed.
|
|
15
|
+
4. Author or update the smallest useful test. Read [authoring-tests.md](references/authoring-tests.md) before changing `.dcua`, context, test data, secrets, or config.
|
|
16
|
+
5. Run the narrowest relevant test and monitor it to completion. Read [running-and-healing.md](references/running-and-healing.md) for commands, targets, artifacts, failure classification, and guarded healing.
|
|
17
|
+
6. Report the outcome, evidence paths, changes made, and whether the failure belongs to the test, environment, or product.
|
|
18
|
+
|
|
19
|
+
## Safety
|
|
20
|
+
|
|
21
|
+
- Never print, open, or echo secret values. Check only whether required keys are configured.
|
|
22
|
+
- Never search for credentials outside the active test project or source another project's `.env` file.
|
|
23
|
+
- Get approval before installing dependencies, running `sudo`, accepting licenses, or changing system configuration.
|
|
24
|
+
- During setup, do not create or edit user tests. Rerun only the failed readiness check, then resume the original task.
|
|
25
|
+
- During healing, edit only `.dcua`, `context.md`, `test-data.md`, or Droid CUA config. Never modify application code under the guise of healing a test.
|
|
26
|
+
- Never weaken or remove an assertion merely to make a failing run pass.
|
|
27
|
+
- Make one minimal change and run the affected test again. Stop after two edit/rerun cycles and present the evidence.
|
|
28
|
+
- Stop immediately when evidence indicates a likely application regression; do not rewrite the test to match broken behavior.
|
|
29
|
+
- Gracefully allow cloud runs to finish cleanup. Avoid terminating a metered device session unless necessary.
|
|
30
|
+
|
|
31
|
+
## Operating principles
|
|
32
|
+
|
|
33
|
+
- Prefer an existing project convention over creating a parallel structure.
|
|
34
|
+
- Prefer headless runs for agent work because they are observable and return deterministic exit codes.
|
|
35
|
+
- Use explicit, user-observable outcomes in tests rather than implementation details or coordinates.
|
|
36
|
+
- Diagnose the first divergence between the instruction and observed behavior before changing anything.
|
|
Binary file
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Authoring tests
|
|
2
|
+
|
|
3
|
+
## Inspect first
|
|
4
|
+
|
|
5
|
+
Find nearby `.dcua` tests and project files before writing. Match their directory layout, naming, vocabulary, starting state, and config conventions. Do not open `.env` or `.secrets`.
|
|
6
|
+
|
|
7
|
+
## Write `.dcua` instructions
|
|
8
|
+
|
|
9
|
+
- Put one clear instruction or assertion on each non-empty line.
|
|
10
|
+
- Describe visible UI intent, not coordinates, selectors, implementation details, or model strategy.
|
|
11
|
+
- Prefer a high-level business intent when `context.md` makes the flow unambiguous. Do not translate every tap and keystroke into a brittle micro-script.
|
|
12
|
+
- Break an instruction down only when the agent demonstrably needs help finding a control or completing part of the flow.
|
|
13
|
+
- State the starting condition when it is not guaranteed by the environment.
|
|
14
|
+
- Use explicit assertions for important outcomes. Prefer stable visible facts such as a screen title, confirmation message, changed value, or navigation result.
|
|
15
|
+
- Use exact visible labels when naming controls or outcomes.
|
|
16
|
+
- Keep each test focused on one behavior. Split unrelated flows.
|
|
17
|
+
- Use `//` comments sparingly for human context; comments are not executed.
|
|
18
|
+
- Never include secret values in the test.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
Sign in using the USER_EMAIL and USER_PASSWORD secrets.
|
|
24
|
+
Assert that the Home screen is visible and shows the signed-in account.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Use the secret key names that exist in `.secrets`; do not inspect their values. If key discovery is necessary, filter the file so only keys reach output, for example `sed -n 's/^\([^#][^=]*\)=.*/\1/p' .secrets`. If a required key is absent, ask the user to add it without sharing its value.
|
|
28
|
+
|
|
29
|
+
## Put knowledge in the right place
|
|
30
|
+
|
|
31
|
+
- Put durable product behavior and terminology in `context.md`, not repeated in every test.
|
|
32
|
+
- Put non-secret reusable people, accounts, products, and scenario combinations in `test-data.md`.
|
|
33
|
+
- Put credentials and sensitive values in `.secrets`.
|
|
34
|
+
- Put reproducible headless settings in `ci/droid-cua.json`.
|
|
35
|
+
- Keep environment and provider credentials in `.env` or the shell/CI secret store.
|
|
36
|
+
|
|
37
|
+
Use `--scenario "<goal>" --test-data <path>` only when the user wants generated permutations or cases. A normal `.dcua` run does not automatically consume `test-data.md`.
|
|
38
|
+
|
|
39
|
+
After editing, show the user the test instructions and explain any new context, data keys, or secret keys required. Never show secret values.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Onboarding and troubleshooting
|
|
2
|
+
|
|
3
|
+
Follow the Desktop Device Setup Wizard sequence: choose a platform, run checks, discover/connect a target, verify runtime access during the requested run, then continue the original task. Run authentication as the separate first preflight described in `project-and-auth.md`.
|
|
4
|
+
|
|
5
|
+
Do not create or modify user test files merely to perform setup. Get approval before installing software, changing system configuration, running `sudo`, or accepting a license. After remediation, rerun only the failed check.
|
|
6
|
+
|
|
7
|
+
## Android
|
|
8
|
+
|
|
9
|
+
Resolve `adb` and `emulator` from `ANDROID_HOME`, `ANDROID_SDK_ROOT`, the macOS default `~/Library/Android/sdk`, the Windows default `%LOCALAPPDATA%\Android\Sdk`, then `PATH`. Respect explicit `DROID_CUA_ANDROID_ADB_PATH` and `DROID_CUA_ANDROID_EMULATOR_PATH` overrides.
|
|
10
|
+
|
|
11
|
+
Run these checks:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
adb version
|
|
15
|
+
emulator -list-avds
|
|
16
|
+
adb devices
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `adb` is blocking.
|
|
20
|
+
- The emulator CLI and existence of an AVD are individually non-blocking.
|
|
21
|
+
- Target availability is blocking and passes when `adb devices` contains a `device` entry or `emulator -list-avds` contains a launchable AVD.
|
|
22
|
+
- For `unauthorized`, ask the user to accept the computer authorization prompt on the device.
|
|
23
|
+
- For `offline`, reconnect the device or restart ADB, then retry.
|
|
24
|
+
- If nothing is available, guide the user to connect a device or create an AVD in Android Studio Device Manager.
|
|
25
|
+
|
|
26
|
+
Use `--avd adb:<serial>` for an attached target and `--avd avd:<name>` to launch an AVD.
|
|
27
|
+
|
|
28
|
+
## iOS
|
|
29
|
+
|
|
30
|
+
Local iOS execution requires macOS. Run checks in this order:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
xcrun --version
|
|
34
|
+
xcode-select -p
|
|
35
|
+
xcodebuild -license check
|
|
36
|
+
xcodebuild -checkFirstLaunchStatus
|
|
37
|
+
node --version
|
|
38
|
+
npm --version
|
|
39
|
+
appium --version
|
|
40
|
+
appium driver list --installed
|
|
41
|
+
xcrun simctl list devices --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Require `xcode-select -p` to point to an `/Xcode*.app/Contents/Developer` path, an installed XCUITest driver, and at least one available iPhone simulator. A booted simulator is helpful but not blocking.
|
|
45
|
+
|
|
46
|
+
Offer only the relevant remediation, with approval:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
|
|
50
|
+
sudo xcodebuild -license accept
|
|
51
|
+
sudo xcodebuild -runFirstLaunch
|
|
52
|
+
npm install -g appium
|
|
53
|
+
appium driver install xcuitest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `--platform ios --avd "<simulator name>"` for the run.
|
|
57
|
+
|
|
58
|
+
## Web
|
|
59
|
+
|
|
60
|
+
Droid CUA automatically discovers Chrome or Edge on macOS and Windows. Check the standard application locations, then run the requested test with `--target web --browser <chrome|edge> --session-mode fresh`. A successful Playwright launch is the runtime verification; do not create a persistent setup-only test.
|
|
61
|
+
|
|
62
|
+
On Linux, automatic installed-browser discovery is not currently supported. Do not claim that a PATH-installed browser will be found.
|
|
63
|
+
|
|
64
|
+
## Cloud targets
|
|
65
|
+
|
|
66
|
+
For LambdaTest, require `LAMBDATEST_USERNAME`, `LAMBDATEST_ACCESS_KEY`, a device name, OS version, and `.apk` or `.ipa`. The CLI uploads the app for each run.
|
|
67
|
+
|
|
68
|
+
For AWS Device Farm, require `AWS_DEVICE_FARM_ACCESS_KEY_ID`, `AWS_DEVICE_FARM_SECRET_ACCESS_KEY`, and `AWS_DEVICE_FARM_PROJECT_ARN`. Discover targets with:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
droid-cua --device-source aws-device-farm --list-devices
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Do not document BrowserStack CLI runs; BrowserStack is not consistently exposed by the current CLI.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Project files and authentication
|
|
2
|
+
|
|
3
|
+
## Project files
|
|
4
|
+
|
|
5
|
+
- `.dcua`: executable test instructions. Each non-empty line is one instruction; `//` begins a comment.
|
|
6
|
+
- `context.md`: durable knowledge about product terminology, screens, navigation, and behavior. Pass it with `--context`, or reference it from `ci/droid-cua.json`.
|
|
7
|
+
- `test-data.md`: non-secret entities and scenario inputs. It is used with `--scenario` and `--test-data`; it is not automatically loaded by every normal test run.
|
|
8
|
+
- `.secrets`: dotenv-style values that Droid CUA may type into the tested application. The model receives the keys, not the resolved values. Headless execution loads this file from the current working directory; `--secrets` overrides it.
|
|
9
|
+
- `.env`: infrastructure credentials and runtime environment variables loaded from the current working directory, including `LOADMILL_API_TOKEN`.
|
|
10
|
+
- `ci/droid-cua.json`: optional checked-in headless config. Paths inside it are relative to the config file.
|
|
11
|
+
- `logs/`: structured JSONL and screenshot artifacts created by `--debug`.
|
|
12
|
+
- HTML reports: self-contained evidence produced by `--report`.
|
|
13
|
+
|
|
14
|
+
Keep `.env` and `.secrets` out of version control. Use `git check-ignore -q .env` and `git check-ignore -q .secrets` to check ignore coverage without opening either file.
|
|
15
|
+
|
|
16
|
+
## Loadmill authentication
|
|
17
|
+
|
|
18
|
+
Loadmill is the default LLM provider. Before a run, check only for presence:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
test -n "${LOADMILL_API_TOKEN:-}" || grep -Eq '^LOADMILL_API_TOKEN=.+$' .env 2>/dev/null
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run this check from the active test project's working directory. Do not print the variable, run `env`, open `.env`, or ask the user to paste the token. Do not search parent directories, sibling repositories, the user's home directory, or the Droid CUA source repository for credentials.
|
|
25
|
+
|
|
26
|
+
If it is missing, instruct the user to:
|
|
27
|
+
|
|
28
|
+
1. Sign in at <https://app.loadmill.com>.
|
|
29
|
+
2. Open **Settings → Security**.
|
|
30
|
+
3. Create an API token with a description such as `Droid CUA CLI`.
|
|
31
|
+
4. Copy it when shown; Loadmill shows it once.
|
|
32
|
+
5. Ensure `.env` is ignored by version control before storing a project token; add `.env` to `.gitignore` if needed.
|
|
33
|
+
6. Save it as `LOADMILL_API_TOKEN=...` in the active test project's `.env`, a shell environment variable, or a CI secret.
|
|
34
|
+
7. Confirm when configuration is complete, without sharing the value.
|
|
35
|
+
|
|
36
|
+
Do not create or populate the token value for the user. Do not copy a token from another file or source another project's `.env`. Sourcing an unrelated file can import development endpoints, proxy settings, and unrelated secrets. When the active project's `.env` contains the key, run `droid-cua` normally from that project; the CLI loads that file automatically.
|
|
37
|
+
|
|
38
|
+
If the user explicitly selects direct OpenAI access, use `--llm-provider openai` with one of the supported OpenAI CUA models: `gpt-5.6-terra`, `gpt-5.6-luna`, or `gpt-5.4`. Check only for the presence of `OPENAI_API_KEY`. If it is missing, instruct the user to create an API key in their OpenAI account and save it in the active project's gitignored `.env`, their shell environment, or a CI secret. Apply the same secret-handling rules: never request, print, inspect, copy, or source the key. Do not silently switch providers to bypass missing Loadmill authentication.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Running, monitoring, and guarded healing
|
|
2
|
+
|
|
3
|
+
## Construct the run
|
|
4
|
+
|
|
5
|
+
Prefer a single test before a directory or scenario run. Preserve an existing provider/model choice in `ci/droid-cua.json`; otherwise use the Loadmill provider. Use Loadmill Smart as the recommended default, or Loadmill Beacon when the user prioritizes faster, lower-cost execution:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
# Recommended default
|
|
9
|
+
--llm-provider loadmill --cua-model loadmill-smart
|
|
10
|
+
|
|
11
|
+
# Faster, lower-cost option
|
|
12
|
+
--llm-provider loadmill --cua-model loadmill-beacon
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Both Loadmill models require `LOADMILL_API_TOKEN`. Do not switch between Smart and Beacon silently; preserve the project's model or explain the speed/cost tradeoff and follow the user's choice. If the user chooses direct OpenAI access instead, require `OPENAI_API_KEY` and use `--llm-provider openai` with one of `gpt-5.6-terra`, `gpt-5.6-luna`, or `gpt-5.4`. Do not mix a provider with another provider's model, silently switch providers, or override an existing project choice without explaining it.
|
|
16
|
+
|
|
17
|
+
Include `--report reports/<test-name>.html` for normal test runs unless the user opts out. Prefer an explicit filename over a directory so the result is easy to locate. Add `--debug` when diagnosis may be needed.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# Attached Android device or running emulator
|
|
21
|
+
droid-cua --llm-provider loadmill --cua-model loadmill-smart --avd adb:<serial> --instructions tests/example.dcua --context tests/context.md --debug --report reports/example.html
|
|
22
|
+
|
|
23
|
+
# Launchable Android AVD
|
|
24
|
+
droid-cua --avd avd:<avd-name> --instructions tests/example.dcua --config ci/droid-cua.json --debug --report reports/example.html
|
|
25
|
+
|
|
26
|
+
# iOS Simulator
|
|
27
|
+
droid-cua --llm-provider loadmill --cua-model loadmill-smart --platform ios --avd "<simulator-name>" --instructions tests/example.dcua --debug --report reports/example.html
|
|
28
|
+
|
|
29
|
+
# Local web
|
|
30
|
+
droid-cua --llm-provider loadmill --cua-model loadmill-smart --target web --browser chrome --session-mode fresh --instructions tests/example.dcua --debug --report reports/example.html
|
|
31
|
+
|
|
32
|
+
# LambdaTest
|
|
33
|
+
droid-cua --llm-provider loadmill --cua-model loadmill-smart --device-source lambdatest --platform android --device-name "<name>" --os-version "<version>" --app <app.apk> --instructions tests/example.dcua --debug --report reports/example.html
|
|
34
|
+
|
|
35
|
+
# AWS Device Farm
|
|
36
|
+
droid-cua --llm-provider loadmill --cua-model loadmill-smart --device-source aws-device-farm --platform android --device-name "<name>" --os-version "<version>" --app <app.apk> --instructions tests/example.dcua --debug --report reports/example.html
|
|
37
|
+
|
|
38
|
+
# Direct OpenAI alternative
|
|
39
|
+
droid-cua --llm-provider openai --cua-model gpt-5.4 --avd adb:<serial> --instructions tests/example.dcua --context tests/context.md --debug --report reports/example.html
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use `.ipa` and `--platform ios` for supported cloud iOS runs. Preserve existing config flags rather than replacing them with defaults.
|
|
43
|
+
|
|
44
|
+
## Monitor and collect evidence
|
|
45
|
+
|
|
46
|
+
Remain attached until the command exits and cloud cleanup finishes. Read terminal progress as it appears; do not treat a long model step as success or failure before completion.
|
|
47
|
+
|
|
48
|
+
- Exit `0`: all requested tests passed.
|
|
49
|
+
- Exit `1`: configuration, connection, execution, or assertion failure.
|
|
50
|
+
- Exit `130`: interrupted run.
|
|
51
|
+
- `--debug`: writes structured execution JSONL and adjacent screenshots under `logs/`.
|
|
52
|
+
- `--report`: writes a self-contained HTML report. Prefer an explicit `.html` file path such as `reports/example.html` so the evidence location is unambiguous.
|
|
53
|
+
|
|
54
|
+
After the run, verify that the report exists and resolve its absolute path. Open it for the user when the agent environment supports local-file opening and policy permits; otherwise provide a clickable local-file link or the absolute path so the user can open it directly.
|
|
55
|
+
|
|
56
|
+
Report the outcome, failing instruction and first divergence when applicable, relevant error text, screenshot/debug/report paths, target, provider/model, and command with secret values omitted.
|
|
57
|
+
|
|
58
|
+
## Classify the first divergence
|
|
59
|
+
|
|
60
|
+
1. **Setup or authentication**: missing/invalid provider credential, dependency, license, browser, or executable. Return to onboarding and rerun only the failed check.
|
|
61
|
+
2. **Target connectivity**: no device, unauthorized/offline ADB state, unavailable simulator, cloud credential/device/app problem, or lost session. Restore the same target, then rerun.
|
|
62
|
+
3. **Test ambiguity or stale test knowledge**: instruction has multiple interpretations, relies on an unstable label, lacks a starting condition, or context contradicts the visible product. A minimal test/context correction is allowed.
|
|
63
|
+
4. **Application regression**: the app crashes, displays an error, violates an explicit requirement, or consistently fails a stable assertion after correct setup. Stop and report a bug with evidence.
|
|
64
|
+
5. **Transient execution**: temporary network, service, model, or timing failure without evidence of a deterministic product/test issue. Retry the unchanged targeted test once.
|
|
65
|
+
|
|
66
|
+
## Guarded healing
|
|
67
|
+
|
|
68
|
+
Before editing, state the classification and evidence. Change only `.dcua`, `context.md`, `test-data.md`, or Droid CUA config. Never edit app code, delete coverage, loosen the expected outcome, or add broad waits simply to obtain green output.
|
|
69
|
+
|
|
70
|
+
Make one minimal change, explain why it corrects the test rather than hides a product defect, and rerun only the affected test. Allow at most two edit/rerun cycles. If it still fails, stop with the original and final evidence, changes attempted, and recommended owner/action.
|