@appliqation/heal-selector 0.1.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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/cli/audit.js +34 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/index.js +142 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.js +37 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/config/env.js +54 -0
- package/dist/config/env.js.map +1 -0
- package/dist/orchestrator/heal.js +76 -0
- package/dist/orchestrator/heal.js.map +1 -0
- package/dist/policy/healPrompt.js +115 -0
- package/dist/policy/healPrompt.js.map +1 -0
- package/dist/tools/codingTools.js +245 -0
- package/dist/tools/codingTools.js.map +1 -0
- package/dist/tools/commandGate.js +60 -0
- package/dist/tools/commandGate.js.map +1 -0
- package/dist/tools/safety.js +18 -0
- package/dist/tools/safety.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Appliqation Pty Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Appliqation Heal-Selector
|
|
2
|
+
|
|
3
|
+
**Heals one broken Playwright selector in an existing, already-working script — narrow and token-efficient, never a full regenerate.**
|
|
4
|
+
|
|
5
|
+
Point it at a script, the selector that's failing, and why — it decides whether that's genuine selector staleness (heal it) or a real behaviour change (decline, not a healing case), verifies its answer against the live page's own accessibility tree, and only ever patches the one line, only after a real `npx playwright test` run confirms the fix actually works.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
A test-set regression run naturally sorts into three buckets: canonical scripts that still pass (nothing to do), test cases with no canonical script at all (a job for [`appliqation-scriptgen`](https://github.com/appliqation/scriptgen)), and canonical scripts that just started failing. That third bucket is ambiguous by nature — did the *app* regress, or did the *script* just go stale (a renamed id, a restructured DOM, the same element findable a different way)? Regenerating the whole script from scratch to answer that is wasteful when the real fix is one selector. This agent is built specifically for that bucket.
|
|
10
|
+
|
|
11
|
+
## The one rule that matters more than anything else here
|
|
12
|
+
|
|
13
|
+
**This agent must never "heal" a selector by finding one that merely makes the assertion pass again.** That's worse than leaving it broken — it fails silently and looks like success. A heal only happens once the model has established, from real evidence, that the new selector targets the *same semantic element* the original one did:
|
|
14
|
+
|
|
15
|
+
- **Accessibility role + accessible name**, read from the live page itself — the primary signal, always available.
|
|
16
|
+
- **The test case's own expected-result text**, from Appliqation — cross-checked against any candidate element.
|
|
17
|
+
- **Recorded session data**, when a caller happens to have it — a bonus signal, never required.
|
|
18
|
+
|
|
19
|
+
No confident match means no heal. It reports a clear decline instead, touches nothing, and hands off to a human or [`appliqation-defect-fix`](https://github.com/appliqation/defect-fix).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @appliqation/heal-selector
|
|
25
|
+
npx playwright install chromium
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Create a `.env` file (in whatever directory you'll run it from) with:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
APPQ_API_KEY=your-appliqation-api-key # read-only is enough
|
|
32
|
+
ANTHROPIC_API_KEY=your-anthropic-key # or OPENAI_API_KEY — pick one
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
appliqation-heal-selector heal \
|
|
37
|
+
--test-case-uuid 2424-8e61a1f0-4bba-4b7b-8fe5-f8ae19e65026 \
|
|
38
|
+
--script-path tests/appliqation/scenario-2424/8e61a1f0.spec.ts \
|
|
39
|
+
--failure "Locator #subscribe-btn not found — likely renamed or restructured" \
|
|
40
|
+
--environment Stage \
|
|
41
|
+
--repo-path /path/to/your/checkout
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Add `--json`/`--ci` for a structured summary. The exit code is 0 only when a patch was written *and* independently verified by a real, post-patch test run — a decline, or an unverified attempt, both exit non-zero; the JSON summary's `declined` field tells them apart.
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
Copy `.env.example` to `.env`. Requires `APPQ_API_KEY` (read-only access is sufficient — this agent never calls an appq write tool) and one of `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`.
|
|
49
|
+
|
|
50
|
+
## Running this safely
|
|
51
|
+
|
|
52
|
+
This agent has real filesystem write access (scoped to `--repo-path`, path-escape and symlink protected), a real shell (allowlisted to a targeted `npx playwright test` and nothing else — no package installation, no bootstrap commands), and a real browser. It never calls an Appliqation write tool and never touches git — a separate agent ([`appliqation-pr-raise`](https://github.com/appliqation/pr-raise)) is responsible for committing and pushing whatever this one patches.
|
|
53
|
+
|
|
54
|
+
**Run this inside a container with an egress allowlist**, same as every agent in this family. This process only ever legitimately needs to reach:
|
|
55
|
+
|
|
56
|
+
- your LLM provider (`api.anthropic.com` or `api.openai.com`)
|
|
57
|
+
- your configured `APPQ_ORIGIN` (`appq.appliqation.io` by default)
|
|
58
|
+
- the site under test — whatever `--environment` resolves to
|
|
59
|
+
|
|
60
|
+
Anything else this process tries to reach is unexpected and worth investigating.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/appliqation/heal-selector.git
|
|
66
|
+
cd heal-selector
|
|
67
|
+
npm install
|
|
68
|
+
cp .env.example .env # fill in APPQ_API_KEY (read-only) and an LLM key
|
|
69
|
+
npm run dev -- heal --test-case-uuid <uuid> --script-path <path> --failure "<text>" --environment <name> --repo-path <path>
|
|
70
|
+
npm run typecheck
|
|
71
|
+
npm test
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
See `CLAUDE.md` for a map of this repo if you're working in it with an AI coding assistant.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Extracted out of cli/index.ts so this is testable without triggering that
|
|
2
|
+
// file's top-level program.parseAsync(process.argv) side effect — same
|
|
3
|
+
// reasoning as appliqation-autotest's cli/resolvers.ts.
|
|
4
|
+
import { safeRecord, safeClose } from '@appliqation/agent-core';
|
|
5
|
+
import { exitCodeFor } from './output.js';
|
|
6
|
+
export async function recordHealRun(args) {
|
|
7
|
+
const { sink, startedAt, endedAt, model, usage, testCaseUuid, scriptPath, result } = args;
|
|
8
|
+
const summary = result
|
|
9
|
+
? {
|
|
10
|
+
testCaseUuid,
|
|
11
|
+
scriptPath,
|
|
12
|
+
writtenPaths: result.writtenPaths,
|
|
13
|
+
declined: result.writtenPaths.length === 0,
|
|
14
|
+
testRan: result.testRun.ran,
|
|
15
|
+
verified: result.testRun.ok,
|
|
16
|
+
report: result.report,
|
|
17
|
+
}
|
|
18
|
+
: undefined;
|
|
19
|
+
await safeRecord(sink, {
|
|
20
|
+
agent: 'appliqation-heal-selector',
|
|
21
|
+
subcommand: 'heal',
|
|
22
|
+
startedAt,
|
|
23
|
+
endedAt,
|
|
24
|
+
durationMillis: endedAt - startedAt,
|
|
25
|
+
model,
|
|
26
|
+
usage,
|
|
27
|
+
turns: result?.turns,
|
|
28
|
+
budgetExceeded: result?.budgetExceeded,
|
|
29
|
+
exitCode: summary ? exitCodeFor(summary) : 1,
|
|
30
|
+
outcome: summary ? { ...summary } : { testCaseUuid, scriptPath, error: true },
|
|
31
|
+
});
|
|
32
|
+
await safeClose(sink);
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/cli/audit.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,uEAAuE;AACvE,wDAAwD;AAExD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAoC,MAAM,yBAAyB,CAAC;AAElG,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAe1C,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAuB;IACzD,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC1F,MAAM,OAAO,GAA4B,MAAM;QAC7C,CAAC,CAAC;YACE,YAAY;YACZ,UAAU;YACV,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,QAAQ,EAAE,MAAM,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;YAC1C,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG;YAC3B,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE;YAC3B,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB;QACH,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,UAAU,CAAC,IAAI,EAAE;QACrB,KAAK,EAAE,2BAA2B;QAClC,UAAU,EAAE,MAAM;QAClB,SAAS;QACT,OAAO;QACP,cAAc,EAAE,OAAO,GAAG,SAAS;QACnC,KAAK;QACL,KAAK;QACL,KAAK,EAAE,MAAM,EAAE,KAAK;QACpB,cAAc,EAAE,MAAM,EAAE,cAAc;QACtC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE;KAC9E,CAAC,CAAC;IACH,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `heal`: repair one broken Playwright selector in an existing, already-
|
|
3
|
+
// working script. See src/orchestrator/heal.ts for the mechanism and
|
|
4
|
+
// src/policy/healPrompt.ts for the actual methodology (no appq-served
|
|
5
|
+
// prompt exists for this yet — the policy is local, bundled here).
|
|
6
|
+
//
|
|
7
|
+
// Generic, caller-agnostic inputs — not designed around any one caller.
|
|
8
|
+
// project_id/scenario_id are always derived from --test-case-uuid, never
|
|
9
|
+
// accepted as separate inputs, same reasoning as every sibling agent.
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { createMcpClient, createAnthropicAdapter, createOpenAiAdapter, createUsageAccumulator, resolveScenarioId, fetchScenarioInfo, resolveUrl, } from '@appliqation/agent-core';
|
|
12
|
+
import { config, resolveProvider, resolveModel } from '../config/env.js';
|
|
13
|
+
import { heal } from '../orchestrator/heal.js';
|
|
14
|
+
import { recordHealRun } from './audit.js';
|
|
15
|
+
import { printJsonSummary, printHumanSummary, exitCodeFor } from './output.js';
|
|
16
|
+
const client = createMcpClient({ origin: config.appqOrigin, apiKey: config.appqApiKey() });
|
|
17
|
+
function buildAdapter() {
|
|
18
|
+
const provider = resolveProvider();
|
|
19
|
+
const model = resolveModel();
|
|
20
|
+
return provider === 'anthropic'
|
|
21
|
+
? createAnthropicAdapter(config.anthropicApiKey, model, config.anthropicMaxTokens)
|
|
22
|
+
: createOpenAiAdapter(config.openaiApiKey, model, config.openaiMaxOutputTokens);
|
|
23
|
+
}
|
|
24
|
+
function logEvent(prefix) {
|
|
25
|
+
return (e) => {
|
|
26
|
+
if (e.type === 'assistant') {
|
|
27
|
+
const text = (e.detail ?? '').trim();
|
|
28
|
+
if (text)
|
|
29
|
+
console.error(`${prefix}[thinking] ${text}`);
|
|
30
|
+
}
|
|
31
|
+
else if (e.type === 'tool') {
|
|
32
|
+
const d = e.detail;
|
|
33
|
+
console.error(`${prefix}[tool] ${d.name} -> ${d.result.slice(0, 300)}`);
|
|
34
|
+
}
|
|
35
|
+
else if (e.type === 'log') {
|
|
36
|
+
console.error(`${prefix}[log] ${e.detail}`);
|
|
37
|
+
}
|
|
38
|
+
else if (e.type === 'usage') {
|
|
39
|
+
const u = e.detail;
|
|
40
|
+
const cacheNote = u.cacheReadTokens
|
|
41
|
+
? ` (${u.cacheReadTokens} from cache)`
|
|
42
|
+
: u.cacheWriteTokens
|
|
43
|
+
? ` (${u.cacheWriteTokens} written to cache)`
|
|
44
|
+
: '';
|
|
45
|
+
console.error(`${prefix}[usage] in=${u.inputTokens} out=${u.outputTokens}${cacheNote}`);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const program = new Command();
|
|
50
|
+
program
|
|
51
|
+
.name('appliqation-heal-selector')
|
|
52
|
+
.description('Heal one broken Playwright selector in an existing script — narrow and token-efficient, never a full ' +
|
|
53
|
+
'regenerate. Verifies a healed selector targets the same semantic element before touching anything; ' +
|
|
54
|
+
'declines rather than guesses. See README.md for the full story.');
|
|
55
|
+
program
|
|
56
|
+
.command('heal')
|
|
57
|
+
.description('Diagnose whether a failing selector is genuine staleness (heal it) or a real behaviour change (decline, ' +
|
|
58
|
+
"not a healing case) — via the live page's accessibility tree, never by picking whatever makes the " +
|
|
59
|
+
"assertion pass. Only ever patches the one locator, and only after a real, post-patch `npx playwright " +
|
|
60
|
+
"test` run confirms it — never the model's own claim. scenario_id/project_id are always derived from " +
|
|
61
|
+
'--test-case-uuid, never accepted as separate inputs.')
|
|
62
|
+
.requiredOption('--test-case-uuid <uuid>', 'test case this failing selector belongs to')
|
|
63
|
+
.requiredOption('--script-path <path>', 'the script file containing the broken selector, relative to --repo-path')
|
|
64
|
+
.requiredOption('--failure <text>', 'free-text description of what is failing and why — a step name, the selector, an error message. Whatever the caller already knows.')
|
|
65
|
+
.requiredOption('--environment <name>', 'environment name — its URL (from get_project_settings) is what the browser navigates to')
|
|
66
|
+
.option('--defect-id <id>', 'a defect linked to this failure, if known — offered as extra diagnostic context')
|
|
67
|
+
.option('--repo-path <path>', 'target repo root every file/command tool call is scoped to', process.cwd())
|
|
68
|
+
.option('--max-turns <n>', 'override BUDGET_MAX_TURNS for this run')
|
|
69
|
+
.option('--json', 'print a single structured JSON summary on stdout instead of a human-readable report')
|
|
70
|
+
.option('--ci', 'shorthand for --json; exit code already reflects the real, execFile-verified outcome either way')
|
|
71
|
+
.action(async (opts) => {
|
|
72
|
+
const json = (opts.json ?? false) || (opts.ci ?? false);
|
|
73
|
+
const adapter = buildAdapter();
|
|
74
|
+
const scenarioId = resolveScenarioId({ testCaseUuid: opts.testCaseUuid });
|
|
75
|
+
const { projectId } = await fetchScenarioInfo(client, scenarioId);
|
|
76
|
+
const url = await resolveUrl(client, opts.environment, projectId);
|
|
77
|
+
const budget = { ...config.budget, ...(opts.maxTurns ? { maxTurns: Number(opts.maxTurns) } : {}) };
|
|
78
|
+
const startedAt = Date.now();
|
|
79
|
+
const usage = createUsageAccumulator();
|
|
80
|
+
const baseLog = logEvent('');
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
result = await heal({
|
|
84
|
+
client,
|
|
85
|
+
adapter,
|
|
86
|
+
testCaseUuid: opts.testCaseUuid,
|
|
87
|
+
defectId: opts.defectId,
|
|
88
|
+
scriptPath: opts.scriptPath,
|
|
89
|
+
failureDescription: opts.failure,
|
|
90
|
+
url,
|
|
91
|
+
repoPath: opts.repoPath,
|
|
92
|
+
budget,
|
|
93
|
+
commandTimeoutMs: config.commandTimeoutMs,
|
|
94
|
+
ringBufferCap: config.ringBufferCap,
|
|
95
|
+
onEvent: (e) => {
|
|
96
|
+
baseLog(e);
|
|
97
|
+
if (e.type === 'usage')
|
|
98
|
+
usage.onUsage(e.detail);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
// Audit write happens whether the run succeeded or threw — see
|
|
104
|
+
// @appliqation/agent-core's audit/sink.ts: safeRecord() (used
|
|
105
|
+
// inside recordHealRun) never lets a failed/unreachable audit sink
|
|
106
|
+
// affect this process's real outcome.
|
|
107
|
+
await recordHealRun({
|
|
108
|
+
sink: config.auditSink,
|
|
109
|
+
startedAt,
|
|
110
|
+
endedAt: Date.now(),
|
|
111
|
+
model: resolveModel(),
|
|
112
|
+
usage: usage.totals(),
|
|
113
|
+
testCaseUuid: opts.testCaseUuid,
|
|
114
|
+
scriptPath: opts.scriptPath,
|
|
115
|
+
result,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (!json) {
|
|
119
|
+
console.log('\n=== Report ===\n');
|
|
120
|
+
console.log(result.report);
|
|
121
|
+
console.error(`\n(${result.turns} turns, budget exceeded: ${result.budgetExceeded})`);
|
|
122
|
+
}
|
|
123
|
+
const summary = {
|
|
124
|
+
testCaseUuid: opts.testCaseUuid,
|
|
125
|
+
scriptPath: opts.scriptPath,
|
|
126
|
+
writtenPaths: result.writtenPaths,
|
|
127
|
+
declined: result.writtenPaths.length === 0,
|
|
128
|
+
testRan: result.testRun.ran,
|
|
129
|
+
verified: result.testRun.ok,
|
|
130
|
+
report: result.report,
|
|
131
|
+
};
|
|
132
|
+
if (json)
|
|
133
|
+
printJsonSummary(summary);
|
|
134
|
+
else
|
|
135
|
+
printHumanSummary(summary);
|
|
136
|
+
process.exitCode = exitCodeFor(summary);
|
|
137
|
+
});
|
|
138
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
139
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// --json/--ci's renderer, matching the family's shape (scriptgen/defect-fix
|
|
2
|
+
// output.ts). exitCodeFor() never trusts the model's own report text — only
|
|
3
|
+
// HealResult.testRun.ok (a real execFile exit code, only counting if it
|
|
4
|
+
// happened after the last write) decides success.
|
|
5
|
+
//
|
|
6
|
+
// `declined` is the one field genuinely new to this agent: a heal attempt
|
|
7
|
+
// has three real outcomes, not two — verified success, a genuine decline
|
|
8
|
+
// (nothing was touched, this isn't a stale-selector case, escalate to a
|
|
9
|
+
// human or defect-fix), or an attempted-but-unverified state (wrote
|
|
10
|
+
// something, verification never happened or failed — needs human review,
|
|
11
|
+
// never silently retried). `writtenPaths.length === 0` is the objective
|
|
12
|
+
// signal for "declined": the policy's own Phase 2/6 instructs it to touch
|
|
13
|
+
// nothing when it can't establish semantic identity, so an empty write set
|
|
14
|
+
// IS the decline, not something inferred from report prose.
|
|
15
|
+
export function printJsonSummary(summary) {
|
|
16
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
17
|
+
}
|
|
18
|
+
export function printHumanSummary(summary) {
|
|
19
|
+
console.log(`\n=== Test case ${summary.testCaseUuid} (${summary.scriptPath}) ===\n`);
|
|
20
|
+
if (summary.declined) {
|
|
21
|
+
console.log(' Declined — not a healing case. No files were touched. See the report for why.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
for (const p of summary.writtenPaths)
|
|
25
|
+
console.log(` patched ${p}`);
|
|
26
|
+
if (!summary.testRan) {
|
|
27
|
+
console.log('\n Never actually ran `npx playwright test` after the patch — not verified.');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log(`\n Verification: ${summary.verified ? 'PASSED' : 'FAILED (or stale — run predates the patch)'}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** 1 unless the heal both wrote something AND was verified — via a real, post-write execFile pass — 0 in that one case, 1 for a decline or an unverified attempt alike. */
|
|
34
|
+
export function exitCodeFor(summary) {
|
|
35
|
+
return summary.testRan && summary.verified ? 0 : 1;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/cli/output.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,4EAA4E;AAC5E,wEAAwE;AACxE,kDAAkD;AAClD,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,wEAAwE;AACxE,oEAAoE;AACpE,yEAAyE;AACzE,wEAAwE;AACxE,0EAA0E;AAC1E,2EAA2E;AAC3E,4DAA4D;AAY5D,MAAM,UAAU,gBAAgB,CAAC,OAAoB;IACnD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAoB;IACpD,OAAO,CAAC,GAAG,CAAC,mBAAmB,OAAO,CAAC,YAAY,KAAK,OAAO,CAAC,UAAU,SAAS,CAAC,CAAC;IACrF,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,iFAAiF,CAAC,CAAC;QAC/F,OAAO;IACT,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,YAAY;QAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;IACrE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,8EAA8E,CAAC,CAAC;IAC9F,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,qBAAqB,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,4CAA4C,EAAE,CAAC,CAAC;IACjH,CAAC;AACH,CAAC;AAED,2KAA2K;AAC3K,MAAM,UAAU,WAAW,CAAC,OAAoB;IAC9C,OAAO,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { DEFAULT_ANTHROPIC_MODEL, DEFAULT_OPENAI_MODEL } from '@appliqation/agent-core/providers';
|
|
3
|
+
import { required, optional } from '@appliqation/agent-core/config';
|
|
4
|
+
import { resolveAuditSink } from '@appliqation/agent-core/audit';
|
|
5
|
+
export const config = {
|
|
6
|
+
appqOrigin: optional('APPQ_ORIGIN') ?? 'https://appq.appliqation.io',
|
|
7
|
+
appqApiKey: () => required('APPQ_API_KEY'),
|
|
8
|
+
anthropicApiKey: optional('ANTHROPIC_API_KEY'),
|
|
9
|
+
openaiApiKey: optional('OPENAI_API_KEY'),
|
|
10
|
+
anthropicModel: optional('ANTHROPIC_MODEL'),
|
|
11
|
+
openaiModel: optional('OPENAI_MODEL'),
|
|
12
|
+
anthropicMaxTokens: Number(optional('ANTHROPIC_MAX_TOKENS') ?? 8192),
|
|
13
|
+
openaiMaxOutputTokens: Number(optional('OPENAI_MAX_OUTPUT_TOKENS') ?? 8192),
|
|
14
|
+
// A single, generous budget — one diagnose-then-heal-then-verify pass,
|
|
15
|
+
// usually much shorter than scriptgen's full draft loop since only one
|
|
16
|
+
// selector is in scope, not a whole script.
|
|
17
|
+
budget: {
|
|
18
|
+
maxCalls: Number(optional('BUDGET_MAX_CALLS') ?? 30),
|
|
19
|
+
// This agent genuinely drives browser_navigate (to inspect the live
|
|
20
|
+
// page's accessibility tree), unlike scriptgen — a real, reachable cap.
|
|
21
|
+
maxPages: Number(optional('BUDGET_MAX_PAGES') ?? 20),
|
|
22
|
+
maxMillis: Number(optional('BUDGET_MAX_MILLIS') ?? 10 * 60 * 1000),
|
|
23
|
+
maxTurns: Number(optional('BUDGET_MAX_TURNS') ?? 30),
|
|
24
|
+
// A broad backstop against runaway spend, not a tuned budget — the other
|
|
25
|
+
// caps above are what normally end a run first. Includes cache tokens.
|
|
26
|
+
maxTotalTokens: Number(optional('BUDGET_MAX_TOTAL_TOKENS') ?? 1_000_000),
|
|
27
|
+
},
|
|
28
|
+
// Wall-clock cap per run_command invocation (a targeted playwright test
|
|
29
|
+
// run) — separate from the overall budget.
|
|
30
|
+
commandTimeoutMs: Number(optional('COMMAND_TIMEOUT_MS') ?? 3 * 60 * 1000),
|
|
31
|
+
// Playwright browser tools' evidence ring-buffer cap — see
|
|
32
|
+
// @appliqation/agent-core's evidence/capture.ts. Small default: this agent
|
|
33
|
+
// looks at one page's accessibility tree, not a multi-step flow.
|
|
34
|
+
ringBufferCap: Number(optional('RING_BUFFER_CAP') ?? 50),
|
|
35
|
+
// Observability, entirely opt-in — see @appliqation/agent-core's audit/sink.ts.
|
|
36
|
+
auditSink: resolveAuditSink({
|
|
37
|
+
auditMongoUri: optional('AUDIT_MONGO_URI'),
|
|
38
|
+
auditMongoDb: optional('AUDIT_MONGO_DB'),
|
|
39
|
+
auditMongoCollection: optional('AUDIT_MONGO_COLLECTION'),
|
|
40
|
+
auditJsonlPath: optional('AUDIT_JSONL_PATH'),
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
export function resolveProvider() {
|
|
44
|
+
if (config.anthropicApiKey)
|
|
45
|
+
return 'anthropic';
|
|
46
|
+
if (config.openaiApiKey)
|
|
47
|
+
return 'openai';
|
|
48
|
+
throw new Error('Set ANTHROPIC_API_KEY or OPENAI_API_KEY');
|
|
49
|
+
}
|
|
50
|
+
export function resolveModel() {
|
|
51
|
+
const provider = resolveProvider();
|
|
52
|
+
return provider === 'anthropic' ? (config.anthropicModel ?? DEFAULT_ANTHROPIC_MODEL) : (config.openaiModel ?? DEFAULT_OPENAI_MODEL);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAClG,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjE,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,UAAU,EAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,6BAA6B;IACpE,UAAU,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;IAC1C,eAAe,EAAE,QAAQ,CAAC,mBAAmB,CAAC;IAC9C,YAAY,EAAE,QAAQ,CAAC,gBAAgB,CAAC;IACxC,cAAc,EAAE,QAAQ,CAAC,iBAAiB,CAAC;IAC3C,WAAW,EAAE,QAAQ,CAAC,cAAc,CAAC;IACrC,kBAAkB,EAAE,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAC,IAAI,IAAI,CAAC;IACpE,qBAAqB,EAAE,MAAM,CAAC,QAAQ,CAAC,0BAA0B,CAAC,IAAI,IAAI,CAAC;IAC3E,uEAAuE;IACvE,uEAAuE;IACvE,4CAA4C;IAC5C,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC;QACpD,oEAAoE;QACpE,wEAAwE;QACxE,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC;QACpD,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;QAClE,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC;QACpD,yEAAyE;QACzE,uEAAuE;QACvE,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAC,IAAI,SAAS,CAAC;KACzE;IACD,wEAAwE;IACxE,2CAA2C;IAC3C,gBAAgB,EAAE,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;IACzE,2DAA2D;IAC3D,2EAA2E;IAC3E,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;IAExD,gFAAgF;IAChF,SAAS,EAAE,gBAAgB,CAAC;QAC1B,aAAa,EAAE,QAAQ,CAAC,iBAAiB,CAAC;QAC1C,YAAY,EAAE,QAAQ,CAAC,gBAAgB,CAAC;QACxC,oBAAoB,EAAE,QAAQ,CAAC,wBAAwB,CAAC;QACxD,cAAc,EAAE,QAAQ,CAAC,kBAAkB,CAAC;KAC7C,CAAC;CACH,CAAC;AAEF,MAAM,UAAU,eAAe;IAC7B,IAAI,MAAM,CAAC,eAAe;QAAE,OAAO,WAAW,CAAC;IAC/C,IAAI,MAAM,CAAC,YAAY;QAAE,OAAO,QAAQ,CAAC;IACzC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC;IACnC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,IAAI,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,IAAI,oBAAoB,CAAC,CAAC;AACtI,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Drives the healing methodology (src/policy/healPrompt.ts — no appq-served
|
|
2
|
+
// prompt exists for this, so runLoop() is used directly, the same pattern
|
|
3
|
+
// appliqation-autopilot uses for its own local policy, not runWorkflow()'s
|
|
4
|
+
// appq-prompt-fetch indirection the rest of this family's "thin" agents use)
|
|
5
|
+
// with three combined tool surfaces: read-only appq context (get_scenario,
|
|
6
|
+
// get_defect_context), a real Playwright browser (to inspect the live
|
|
7
|
+
// page's accessibility tree — the grounding signal for a healed selector's
|
|
8
|
+
// semantic identity), and real coding tools (read/patch the script, run a
|
|
9
|
+
// targeted verification). No agent in this family combined browser tools
|
|
10
|
+
// with coding tools before this — the composition itself is a direct,
|
|
11
|
+
// low-risk extension of the flat-tool-array + name-prefix-dispatch pattern
|
|
12
|
+
// appliqation-explorer already uses for browser+appq.
|
|
13
|
+
import { chromium } from 'playwright';
|
|
14
|
+
import { runLoop, fetchAppqToolDefs, createGatedAppqDispatcher, PlaywrightBrowserTools, BROWSER_TOOL_DEFS, } from '@appliqation/agent-core';
|
|
15
|
+
import { READONLY_CONTEXT_TOOLS } from '../tools/safety.js';
|
|
16
|
+
import { CODING_TOOL_DEFS, CodingTools } from '../tools/codingTools.js';
|
|
17
|
+
import { buildHealPrompt } from '../policy/healPrompt.js';
|
|
18
|
+
function seedMessage(opts) {
|
|
19
|
+
const lines = [
|
|
20
|
+
`Test case UUID: ${opts.testCaseUuid}`,
|
|
21
|
+
`Script to heal: ${opts.scriptPath}`,
|
|
22
|
+
`What's failing: ${opts.failureDescription}`,
|
|
23
|
+
`Live page URL (for browser_navigate): ${opts.url}`,
|
|
24
|
+
`Target repo root (every file/command tool call is scoped here): ${opts.repoPath}`,
|
|
25
|
+
];
|
|
26
|
+
if (opts.defectId)
|
|
27
|
+
lines.push(`Linked defect ID: ${opts.defectId}`);
|
|
28
|
+
lines.push('Begin now — start with read_file on the script.');
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
|
31
|
+
export async function heal(opts) {
|
|
32
|
+
const coding = new CodingTools(opts.repoPath, opts.commandTimeoutMs);
|
|
33
|
+
const appqToolDefs = await fetchAppqToolDefs(opts.client, READONLY_CONTEXT_TOOLS);
|
|
34
|
+
const gatedAppq = createGatedAppqDispatcher(opts.client, READONLY_CONTEXT_TOOLS);
|
|
35
|
+
const codingToolNames = new Set(CODING_TOOL_DEFS.map((t) => t.name));
|
|
36
|
+
const browser = await chromium.launch();
|
|
37
|
+
const page = await browser.newPage();
|
|
38
|
+
try {
|
|
39
|
+
const browserTools = new PlaywrightBrowserTools(page, opts.ringBufferCap);
|
|
40
|
+
const dispatch = async (name, args) => {
|
|
41
|
+
if (name.startsWith('browser_'))
|
|
42
|
+
return browserTools.dispatch(name, args);
|
|
43
|
+
if (codingToolNames.has(name))
|
|
44
|
+
return coding.dispatch(name, args);
|
|
45
|
+
return gatedAppq(name, args);
|
|
46
|
+
};
|
|
47
|
+
const loopResult = await runLoop({
|
|
48
|
+
adapter: opts.adapter,
|
|
49
|
+
system: buildHealPrompt(),
|
|
50
|
+
seedMessage: seedMessage(opts),
|
|
51
|
+
tools: [...appqToolDefs, ...BROWSER_TOOL_DEFS, ...CODING_TOOL_DEFS],
|
|
52
|
+
dispatch,
|
|
53
|
+
budget: opts.budget,
|
|
54
|
+
onEvent: opts.onEvent,
|
|
55
|
+
});
|
|
56
|
+
const writtenPaths = coding.getWrittenPaths();
|
|
57
|
+
const lastTestRun = coding.lastPlaywrightTestRun();
|
|
58
|
+
const lastWriteAt = writtenPaths.size > 0 ? Math.max(...writtenPaths.values()) : 0;
|
|
59
|
+
const verifiedAfterLastWrite = lastTestRun !== null && lastTestRun.ok && lastTestRun.timestamp >= lastWriteAt;
|
|
60
|
+
return {
|
|
61
|
+
report: loopResult.report,
|
|
62
|
+
turns: loopResult.turns,
|
|
63
|
+
budgetExceeded: loopResult.budgetExceeded,
|
|
64
|
+
writtenPaths: [...writtenPaths.keys()],
|
|
65
|
+
testRun: {
|
|
66
|
+
ran: lastTestRun !== null,
|
|
67
|
+
ok: verifiedAfterLastWrite,
|
|
68
|
+
exitCode: lastTestRun?.exitCode ?? null,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await browser.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=heal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heal.js","sourceRoot":"","sources":["../../src/orchestrator/heal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The actual decision-making methodology. There is no appq-served prompt
|
|
2
|
+
// for this — unlike appliqation-scriptgen/-defect-fix (deliberately thin,
|
|
3
|
+
// methodology owned by appq:automate/appq:fix), healing a selector has no
|
|
4
|
+
// server-side equivalent yet, so this is a local, bundled prompt, authored
|
|
5
|
+
// here and driven through @appliqation/agent-core's runLoop() directly —
|
|
6
|
+
// the same pattern appliqation-autopilot uses for its own local policy, not
|
|
7
|
+
// the runWorkflow()-against-a-served-prompt pattern the rest of this
|
|
8
|
+
// family's "thin" agents use.
|
|
9
|
+
export function buildHealPrompt() {
|
|
10
|
+
return `You are a narrow, careful specialist: you heal ONE broken selector in an existing, \
|
|
11
|
+
already-working Playwright script. You are not appliqation-scriptgen — you never draft a script \
|
|
12
|
+
from scratch, never touch anything beyond the one locator you were asked about, and never widen \
|
|
13
|
+
scope on your own initiative.
|
|
14
|
+
|
|
15
|
+
**Non-negotiable, more important than anything else in this prompt:** you must never "heal" a \
|
|
16
|
+
selector by finding one that merely makes the assertion pass again. That is actively worse than \
|
|
17
|
+
leaving it broken — it fails silently and looks like success. A heal is only valid if you have \
|
|
18
|
+
established that the new selector targets the SAME semantic element the original one did. If you \
|
|
19
|
+
cannot establish that with real evidence, you must decline — report that this is not a healing \
|
|
20
|
+
case, touch nothing, and let a human or appliqation-defect-fix take it from there. When in doubt, \
|
|
21
|
+
decline. A wrong decline costs a follow-up; a wrong heal corrodes the thing this whole test exists \
|
|
22
|
+
to protect.
|
|
23
|
+
|
|
24
|
+
**You only ever touch a locator.** If the locator you were pointed at actually resolves to a real \
|
|
25
|
+
element fine — the failure turns out to live in an assertion, an expected value, a wait condition, \
|
|
26
|
+
anything that isn't "this selector can't find what it used to find" — that is not a healing case \
|
|
27
|
+
either, even if you can tell exactly what the "correct" assertion should say. Decline. Rewriting an \
|
|
28
|
+
assertion to match either the live page or the test case's own \`expected_result\` text is a \
|
|
29
|
+
judgment call about what the test SHOULD verify, not a mechanical repair of how it finds an \
|
|
30
|
+
element — that call belongs to a human or appliqation-defect-fix, not to you, no matter how \
|
|
31
|
+
confident the evidence looks. Same bar as above: when in doubt, decline.
|
|
32
|
+
|
|
33
|
+
## Phase 0 — Prerequisites
|
|
34
|
+
|
|
35
|
+
You have three tool surfaces: read-only Appliqation context (\`get_scenario\`, and \
|
|
36
|
+
\`get_defect_context\` if a defect ID was given), real browser tools (\`browser_*\` — a live \
|
|
37
|
+
Chromium page you can navigate and inspect), and real coding tools (\`read_file\`/\`write_file\`/\
|
|
38
|
+
\`list_directory\`/\`run_command\`, scoped to the target repo). \`run_command\` only allows a \
|
|
39
|
+
targeted \`npx playwright test\` (plus \`node --version\`/\`git status\`/\`git diff\`) — never a \
|
|
40
|
+
full-suite run, never package installation. This repo is already fully set up; you are healing \
|
|
41
|
+
one line, not bootstrapping anything.
|
|
42
|
+
|
|
43
|
+
## Phase 1 — Load context
|
|
44
|
+
|
|
45
|
+
1. \`read_file\` the target script. Find the specific locator/step you were told is failing — the \
|
|
46
|
+
seed message names the file and, when known, the step/selector and the error that surfaced it.
|
|
47
|
+
2. Call \`get_scenario\` to load this test case's own \`steps\`/\`expected_result\` text — the \
|
|
48
|
+
ground truth for what this step is actually supposed to verify. This, plus whatever the script's \
|
|
49
|
+
own surrounding code/comments reveal (variable names, nearby assertions), is what a candidate \
|
|
50
|
+
element's identity gets checked against.
|
|
51
|
+
3. If a defect ID was given, \`get_defect_context\` for additional evidence (routes_visited, \
|
|
52
|
+
console/network errors) — optional context, not required to proceed.
|
|
53
|
+
|
|
54
|
+
## Phase 2 — Diagnose on the live page
|
|
55
|
+
|
|
56
|
+
Navigate to the relevant state (\`browser_navigate\`, then whatever steps are needed to reach the \
|
|
57
|
+
point the failing step exercises) and take an accessibility snapshot (\`browser_snapshot\` — \
|
|
58
|
+
\`page.ariaSnapshot\`). You are looking for one thing: **does an element with the same role and \
|
|
59
|
+
accessible name the original selector was written to target still genuinely exist**, just findable \
|
|
60
|
+
a different way (DOM restructured, an id/class renamed, a wrapper added) — or has the actual \
|
|
61
|
+
element/behavior changed or disappeared?
|
|
62
|
+
|
|
63
|
+
- **Same role + name, clearly the same UI element** (e.g. a \`button\` named "Subscribe" that used \
|
|
64
|
+
to have \`id="subscribe-btn"\` and now doesn't, but a button with that exact accessible name is \
|
|
65
|
+
still right there) → this is genuine staleness. Proceed to Phase 3.
|
|
66
|
+
- **No element with a matching role+name exists at all, or the closest candidate is a genuinely \
|
|
67
|
+
different control** → this is not a stale selector, it's a real change. Do not heal. Skip to \
|
|
68
|
+
Phase 5 and report a decline.
|
|
69
|
+
- **The original locator actually still resolves and works** — it finds the same element it always \
|
|
70
|
+
did, the click/interaction succeeds, and the failure is in what happens *after* (a wrong assertion, \
|
|
71
|
+
a stale expected value, a wait condition that doesn't hold) → there is no broken selector here to \
|
|
72
|
+
heal. Do not "fix" the assertion instead. Skip to Phase 5 and report a decline, same as the case \
|
|
73
|
+
above.
|
|
74
|
+
- **Rrweb original-interaction data**, if the seed message provided any, is a bonus signal for \
|
|
75
|
+
what a human actually interacted with — use it when present to corroborate role+name, but it will \
|
|
76
|
+
often be absent. Never treat its absence as a reason to lower your bar; role+name and the TC's own \
|
|
77
|
+
\`expected_result\` text carry the weight either way.
|
|
78
|
+
- If you are not confident which candidate (if any) is the same element, that is itself a decline \
|
|
79
|
+
— do not pick the "closest" one out of several plausible options and hope.
|
|
80
|
+
|
|
81
|
+
## Phase 3 — Compose the healed locator
|
|
82
|
+
|
|
83
|
+
Only reached if Phase 2 established genuine semantic identity. Prefer a locator built the same way \
|
|
84
|
+
the rest of the script already locates elements (role+name via \`getByRole\`, if that is this \
|
|
85
|
+
script's own convention; otherwise match its existing style) — the goal is a locator that will \
|
|
86
|
+
survive the *next* incidental DOM change too, not just this one.
|
|
87
|
+
|
|
88
|
+
## Phase 4 — Patch narrowly
|
|
89
|
+
|
|
90
|
+
\`write_file\` the ENTIRE file with only the one locator line changed — read the current full \
|
|
91
|
+
content first (Phase 1 already did this), then write back the same content with that one line \
|
|
92
|
+
replaced. Do not reformat, do not touch unrelated selectors or assertions, do not "clean up" \
|
|
93
|
+
anything else you notice while you're in there. This is a targeted repair, not a rewrite.
|
|
94
|
+
|
|
95
|
+
## Phase 5 — Verify for real
|
|
96
|
+
|
|
97
|
+
Run the specific test via \`run_command\` (\`npx playwright test --grep "<test name>"\`, or the \
|
|
98
|
+
spec's own path). This is the only thing that can turn a heal into a verified one — your own \
|
|
99
|
+
belief that the new selector is correct is not sufficient, and never claim success without this \
|
|
100
|
+
step actually having run, after your write, with a real passing exit code.
|
|
101
|
+
|
|
102
|
+
## Phase 6 — Report
|
|
103
|
+
|
|
104
|
+
State plainly which outcome this was:
|
|
105
|
+
- **Healed and verified**: cite the exact role+name evidence that established identity, quote the \
|
|
106
|
+
before/after locator, and confirm the real \`npx playwright test\` result you observed.
|
|
107
|
+
- **Declined**: state exactly what you looked for and what you found (or didn't) on the live page, \
|
|
108
|
+
and why — no matching candidate existed, or the original locator worked fine and the real failure \
|
|
109
|
+
was elsewhere (an assertion/expected value, not the selector). Either way, recommend this go to a \
|
|
110
|
+
human or a defect-investigation, not another healing attempt.
|
|
111
|
+
|
|
112
|
+
Never blur these two into something in between — a human reading your report should be able to \
|
|
113
|
+
tell immediately whether their script changed or not, and why.`;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=healPrompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"healPrompt.js","sourceRoot":"","sources":["../../src/policy/healPrompt.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,0EAA0E;AAC1E,0EAA0E;AAC1E,2EAA2E;AAC3E,yEAAyE;AACzE,4EAA4E;AAC5E,qEAAqE;AACrE,8BAA8B;AAE9B,MAAM,UAAU,eAAe;IAC7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+DAuGsD,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// The filesystem + shell surface needed to read the existing script, patch
|
|
2
|
+
// the one broken locator, and actually run the result. Every path is scoped
|
|
3
|
+
// to repoPath (no traversal out of it); every command goes through
|
|
4
|
+
// commandGate.ts's allowlist AND is spawned via execFile with an explicit
|
|
5
|
+
// argv array — never a shell string — so the OS never parses arguments as
|
|
6
|
+
// shell syntax in the first place. Copied verbatim from
|
|
7
|
+
// appliqation-scriptgen/src/tools/codingTools.ts — the hardening here is
|
|
8
|
+
// fully general, nothing about it is scriptgen-specific.
|
|
9
|
+
import { readFile, writeFile, mkdir, readdir, realpath } from 'node:fs/promises';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { resolve, dirname, relative, isAbsolute } from 'node:path';
|
|
12
|
+
import { execFile } from 'node:child_process';
|
|
13
|
+
import { assertCommandAllowed } from './commandGate.js';
|
|
14
|
+
// Hand-rolled rather than util.promisify(execFile) — Node's real execFile
|
|
15
|
+
// resolves {stdout, stderr} via an internal util.promisify.custom symbol
|
|
16
|
+
// that a mocked module in tests won't have, so promisify(execFile) silently
|
|
17
|
+
// degrades to generic single-value promisify (stdout only, stderr dropped)
|
|
18
|
+
// under mocking. This wrapper has one obvious behavior either way.
|
|
19
|
+
function execFileAsync(command, args, options) {
|
|
20
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
21
|
+
execFile(command, args, options, (error, stdout, stderr) => {
|
|
22
|
+
if (error) {
|
|
23
|
+
const failure = error;
|
|
24
|
+
failure.stdout = String(stdout ?? '');
|
|
25
|
+
failure.stderr = String(stderr ?? '');
|
|
26
|
+
rejectPromise(failure);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
resolvePromise({ stdout: String(stdout ?? ''), stderr: String(stderr ?? '') });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// execFile inherits the FULL parent process env by default — this would hand
|
|
35
|
+
// ANTHROPIC_API_KEY/APPQ_API_KEY (this agent's own MCP credential) straight to
|
|
36
|
+
// npx and to any lifecycle script a dependency runs. But `npx playwright test`
|
|
37
|
+
// runs the real target script, typically built on @appliqation/automation-sdk,
|
|
38
|
+
// which reads its own env vars to authenticate and report — most critically
|
|
39
|
+
// APPLIQATION_API_KEY (the project's own reporting key, a different credential
|
|
40
|
+
// from this agent's APPQ_API_KEY) and APPQ_AUTH_STATE_DIR in setupAuth(). This
|
|
41
|
+
// list is deliberately explicit — see automation-sdk-js's README/.env.example
|
|
42
|
+
// for the vocabulary — not a blanket APPQ_/APPLIQATION_ prefix passthrough,
|
|
43
|
+
// which would also leak this agent's own APPQ_API_KEY.
|
|
44
|
+
const SAFE_ENV_VARS = [
|
|
45
|
+
'PATH',
|
|
46
|
+
'HOME',
|
|
47
|
+
'LANG',
|
|
48
|
+
'LC_ALL',
|
|
49
|
+
'TMPDIR',
|
|
50
|
+
'CI',
|
|
51
|
+
'NODE_ENV',
|
|
52
|
+
'APPLIQATION_API_KEY',
|
|
53
|
+
'APPLIQATION_PROJECT_KEY',
|
|
54
|
+
'APPLIQATION_BASE_URL',
|
|
55
|
+
'APPLIQATION_SUT_BASE_URL',
|
|
56
|
+
'APPLIQATION_APP_URL',
|
|
57
|
+
'APPLIQATION_ENVIRONMENT',
|
|
58
|
+
'APPLIQATION_SCENARIO_ID',
|
|
59
|
+
'APPLIQATION_RUN_ID',
|
|
60
|
+
'APPLIQATION_RUN_TITLE',
|
|
61
|
+
'APPLIQATION_USE_JWT_AUTH',
|
|
62
|
+
'APPLIQATION_ON_SCOPE_MISMATCH',
|
|
63
|
+
'APPLIQATION_AUTO_TAG_ENABLED',
|
|
64
|
+
'APPLIQATION_AUTO_TAG_NAME',
|
|
65
|
+
'APPLIQATION_ENABLE',
|
|
66
|
+
'APPQ_ENABLE',
|
|
67
|
+
'APPQ_AUTH_STATE_DIR',
|
|
68
|
+
'APPQ_LOGIN_FILE',
|
|
69
|
+
];
|
|
70
|
+
// Per-project/role SUT credentials a generated login.ts reads
|
|
71
|
+
// (APPQ_PROJECT_<id>_<ROLE>_USERNAME/_PASSWORD/_API_KEY/_API_HEADER_NAME —
|
|
72
|
+
// see @appliqation/agent-core's authState.ts). A narrow shape, not a blanket
|
|
73
|
+
// APPQ_ prefix, specifically so this agent's own APPQ_API_KEY (no
|
|
74
|
+
// _PROJECT_<id>_ segment) can never match it.
|
|
75
|
+
const SAFE_ENV_PATTERN = /^APPQ_PROJECT_\d+_[A-Z0-9_]+$/;
|
|
76
|
+
function safeChildEnv() {
|
|
77
|
+
const env = {};
|
|
78
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
continue;
|
|
81
|
+
if (SAFE_ENV_VARS.includes(key) || SAFE_ENV_PATTERN.test(key))
|
|
82
|
+
env[key] = value;
|
|
83
|
+
}
|
|
84
|
+
return env;
|
|
85
|
+
}
|
|
86
|
+
export const CODING_TOOL_DEFS = [
|
|
87
|
+
{
|
|
88
|
+
name: 'read_file',
|
|
89
|
+
description: 'Read a text file, relative to the target repo root.',
|
|
90
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'write_file',
|
|
94
|
+
description: 'Write (create or overwrite) a text file, relative to the target repo root. Creates parent ' +
|
|
95
|
+
'directories as needed. Use this to patch the one broken locator — only write what you are ' +
|
|
96
|
+
'confident is a genuine, targeted fix, never a speculative rewrite.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: { path: { type: 'string' }, content: { type: 'string' } },
|
|
100
|
+
required: ['path', 'content'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'list_directory',
|
|
105
|
+
description: 'List entries (files and directories) in a directory, relative to the target repo root.',
|
|
106
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: [] },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'run_command',
|
|
110
|
+
description: 'Run an allowlisted shell command (npx playwright test, node --version, git status/diff) in the ' +
|
|
111
|
+
'target repo. Anything outside that allowlist is refused before it runs. Use this to verify a heal ' +
|
|
112
|
+
'for real — never claim a fix works without actually running the test here.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
command: { type: 'string', description: 'The binary, e.g. "npx", "node", "git".' },
|
|
117
|
+
args: { type: 'array', items: { type: 'string' }, description: 'Argv, one element per argument.' },
|
|
118
|
+
},
|
|
119
|
+
required: ['command', 'args'],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
/** Wraps a real filesystem + shell surface, scoped to one repo root, tracking what actually happened. */
|
|
124
|
+
export class CodingTools {
|
|
125
|
+
repoPath;
|
|
126
|
+
commandTimeoutMs;
|
|
127
|
+
writtenPaths = new Map(); // relative path -> last-written timestamp
|
|
128
|
+
commandHistory = [];
|
|
129
|
+
constructor(repoPath, commandTimeoutMs) {
|
|
130
|
+
this.repoPath = repoPath;
|
|
131
|
+
this.commandTimeoutMs = commandTimeoutMs;
|
|
132
|
+
}
|
|
133
|
+
async resolveScoped(relPath) {
|
|
134
|
+
const resolved = resolve(this.repoPath, relPath);
|
|
135
|
+
const rel = relative(this.repoPath, resolved);
|
|
136
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
137
|
+
throw new Error(`Path "${relPath}" escapes the target repo root — refusing.`);
|
|
138
|
+
}
|
|
139
|
+
// The check above is purely lexical — a symlink whose own path sits
|
|
140
|
+
// inside repoPath but whose target resolves outside it would pass. Walk
|
|
141
|
+
// up to the nearest existing ancestor (the target itself may not exist
|
|
142
|
+
// yet — write_file creates new files/directories) and confirm ITS real,
|
|
143
|
+
// symlink-resolved path also stays within the repo root's real path.
|
|
144
|
+
let probe = resolved;
|
|
145
|
+
while (!existsSync(probe)) {
|
|
146
|
+
const parent = dirname(probe);
|
|
147
|
+
if (parent === probe)
|
|
148
|
+
break; // hit the filesystem root; let the real fs op fail naturally
|
|
149
|
+
probe = parent;
|
|
150
|
+
}
|
|
151
|
+
const [rootReal, probeReal] = await Promise.all([realpath(this.repoPath), realpath(probe)]);
|
|
152
|
+
const realRel = relative(rootReal, probeReal);
|
|
153
|
+
if (realRel.startsWith('..') || isAbsolute(realRel)) {
|
|
154
|
+
throw new Error(`Path "${relPath}" escapes the target repo root via a symlink — refusing.`);
|
|
155
|
+
}
|
|
156
|
+
return resolved;
|
|
157
|
+
}
|
|
158
|
+
/** Relative paths written so far, each with the timestamp of its most recent write. */
|
|
159
|
+
getWrittenPaths() {
|
|
160
|
+
return new Map(this.writtenPaths);
|
|
161
|
+
}
|
|
162
|
+
getCommandHistory() {
|
|
163
|
+
return [...this.commandHistory];
|
|
164
|
+
}
|
|
165
|
+
/** The most recent `npx playwright test` invocation's real, execFile-reported outcome — or null if none ran. */
|
|
166
|
+
lastPlaywrightTestRun() {
|
|
167
|
+
for (let i = this.commandHistory.length - 1; i >= 0; i--) {
|
|
168
|
+
const r = this.commandHistory[i];
|
|
169
|
+
if (r.command === 'npx' && r.args[0] === 'playwright' && r.args[1] === 'test')
|
|
170
|
+
return r;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
async dispatch(name, args) {
|
|
175
|
+
switch (name) {
|
|
176
|
+
case 'read_file': {
|
|
177
|
+
const rawPath = String(args.path ?? '');
|
|
178
|
+
try {
|
|
179
|
+
const content = await readFile(await this.resolveScoped(rawPath), 'utf-8');
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
text: content.length > 50_000 ? `${content.slice(0, 50_000)}\n... (truncated)` : content,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return { ok: false, text: `Could not read "${rawPath}": ${err.message}` };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
case 'write_file': {
|
|
190
|
+
const rawPath = String(args.path ?? '');
|
|
191
|
+
const content = String(args.content ?? '');
|
|
192
|
+
const resolved = await this.resolveScoped(rawPath);
|
|
193
|
+
await mkdir(dirname(resolved), { recursive: true });
|
|
194
|
+
await writeFile(resolved, content, 'utf-8');
|
|
195
|
+
this.writtenPaths.set(rawPath, Date.now());
|
|
196
|
+
return { ok: true, text: `Wrote ${content.length} bytes to ${rawPath}` };
|
|
197
|
+
}
|
|
198
|
+
case 'list_directory': {
|
|
199
|
+
const rawPath = String(args.path ?? '.');
|
|
200
|
+
try {
|
|
201
|
+
const entries = await readdir(await this.resolveScoped(rawPath), { withFileTypes: true });
|
|
202
|
+
const lines = entries.map((e) => `${e.isDirectory() ? 'dir ' : 'file'} ${e.name}`).sort();
|
|
203
|
+
return { ok: true, text: lines.join('\n') || '(empty directory)' };
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
return { ok: false, text: `Could not list "${rawPath}": ${err.message}` };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
case 'run_command': {
|
|
210
|
+
const command = String(args.command ?? '');
|
|
211
|
+
const cmdArgs = Array.isArray(args.args) ? args.args.map(String) : [];
|
|
212
|
+
try {
|
|
213
|
+
assertCommandAllowed(command, cmdArgs);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return { ok: false, text: err.message };
|
|
217
|
+
}
|
|
218
|
+
return this.runAllowed(command, cmdArgs);
|
|
219
|
+
}
|
|
220
|
+
default:
|
|
221
|
+
return { ok: false, text: `Unknown coding tool "${name}"` };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async runAllowed(command, cmdArgs) {
|
|
225
|
+
try {
|
|
226
|
+
const { stdout, stderr } = await execFileAsync(command, cmdArgs, {
|
|
227
|
+
cwd: this.repoPath,
|
|
228
|
+
timeout: this.commandTimeoutMs,
|
|
229
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
230
|
+
env: safeChildEnv(),
|
|
231
|
+
});
|
|
232
|
+
this.commandHistory.push({ command, args: cmdArgs, exitCode: 0, ok: true, timestamp: Date.now() });
|
|
233
|
+
const out = `${stdout}${stderr ? `\n[stderr]\n${stderr}` : ''}`.trim();
|
|
234
|
+
return { ok: true, text: out.length > 20_000 ? out.slice(-20_000) : out || '(no output, exit 0)' };
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
const e = err;
|
|
238
|
+
const exitCode = typeof e.code === 'number' ? e.code : null;
|
|
239
|
+
this.commandHistory.push({ command, args: cmdArgs, exitCode, ok: false, timestamp: Date.now() });
|
|
240
|
+
const out = `${e.stdout ?? ''}\n[stderr]\n${e.stderr ?? e.message}`.trim();
|
|
241
|
+
return { ok: false, text: out.length > 20_000 ? out.slice(-20_000) : out };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=codingTools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codingTools.js","sourceRoot":"","sources":["../../src/tools/codingTools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// This agent's own real shell capability — needed only to actually run the
|
|
2
|
+
// targeted `npx playwright test` verification after a heal, same mechanism
|
|
3
|
+
// appliqation-scriptgen/appliqation-defect-fix already established for the
|
|
4
|
+
// same reason.
|
|
5
|
+
//
|
|
6
|
+
// Hardcoded, non-negotiable invariant (same class of thing as
|
|
7
|
+
// destructiveActionGate.ts / the appq tool allowlists elsewhere in this
|
|
8
|
+
// family): an explicit allowlist of (command, argv-shape), checked BEFORE
|
|
9
|
+
// execution, never widened by prompt text. The actual execution path
|
|
10
|
+
// (codingTools.ts) uses child_process.execFile with an argv array — never a
|
|
11
|
+
// shell string — so even an allowed command can't smuggle a second command
|
|
12
|
+
// via `;`/`&&`/backticks; the OS never parses the arguments as shell syntax
|
|
13
|
+
// in the first place. The allowlist is defense in depth on top of that, not
|
|
14
|
+
// the only layer.
|
|
15
|
+
//
|
|
16
|
+
// Deliberately narrower than scriptgen/defect-fix's allowlist: this agent
|
|
17
|
+
// operates on an already-set-up repo with an already-working script — it
|
|
18
|
+
// never bootstraps a project or installs packages, so there's no `npm
|
|
19
|
+
// init`/`npm install`/`npx playwright install` case here at all. Only
|
|
20
|
+
// `npx playwright test` (targeted — a `--grep`/spec-path pair, never a full
|
|
21
|
+
// suite) plus basic read-only sanity commands.
|
|
22
|
+
/** A path-shaped argument (a test file glob/relative path) — no traversal, no shell metacharacters. */
|
|
23
|
+
function isSafePathArg(arg) {
|
|
24
|
+
return !arg.includes('..') && !/[;&|`$(){}<>]/.test(arg) && !arg.startsWith('-');
|
|
25
|
+
}
|
|
26
|
+
const ALLOWED_COMMANDS = {
|
|
27
|
+
npx: (args) => {
|
|
28
|
+
if (args[0] !== 'playwright')
|
|
29
|
+
return false;
|
|
30
|
+
if (args[1] === '--version' && args.length === 2)
|
|
31
|
+
return true;
|
|
32
|
+
if (args[1] === 'test') {
|
|
33
|
+
// A --grep "<title>" pair and/or a relative spec file path — always
|
|
34
|
+
// targeted at the one test case being healed, never a bare, unscoped
|
|
35
|
+
// full-suite run.
|
|
36
|
+
return args.slice(2).every((a) => a === '--grep' || isSafePathArg(a));
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
},
|
|
40
|
+
node: (args) => args.length === 1 && args[0] === '--version',
|
|
41
|
+
git: (args) => {
|
|
42
|
+
if (args.length === 1 && args[0] === 'status')
|
|
43
|
+
return true;
|
|
44
|
+
if (args.length === 1 && args[0] === 'diff')
|
|
45
|
+
return true;
|
|
46
|
+
if (args.length === 2 && args[0] === 'diff' && args[1] === '--stat')
|
|
47
|
+
return true;
|
|
48
|
+
return false;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export function assertCommandAllowed(command, args) {
|
|
52
|
+
const validator = ALLOWED_COMMANDS[command];
|
|
53
|
+
if (!validator || !validator(args)) {
|
|
54
|
+
throw new Error(`Command "${command} ${args.join(' ')}" is not in the allowlist. This is a hardcoded boundary — no ` +
|
|
55
|
+
`workflow prompt can widen it. Allowed: npx playwright --version/test (targeted — --grep and/or a spec ` +
|
|
56
|
+
`path), node --version, git status/diff. No npm install/init or npx playwright install — this agent ` +
|
|
57
|
+
`never bootstraps a project, only heals an existing, already-working one.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=commandGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commandGate.js","sourceRoot":"","sources":["../../src/tools/commandGate.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,2EAA2E;AAC3E,2EAA2E;AAC3E,eAAe;AACf,EAAE;AACF,8DAA8D;AAC9D,wEAAwE;AACxE,0EAA0E;AAC1E,qEAAqE;AACrE,4EAA4E;AAC5E,2EAA2E;AAC3E,4EAA4E;AAC5E,4EAA4E;AAC5E,kBAAkB;AAClB,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,sEAAsE;AACtE,sEAAsE;AACtE,4EAA4E;AAC5E,+CAA+C;AAE/C,uGAAuG;AACvG,SAAS,aAAa,CAAC,GAAW;IAChC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AACnF,CAAC;AAID,MAAM,gBAAgB,GAA8B;IAClD,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE;QACZ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,YAAY;YAAE,OAAO,KAAK,CAAC;QAC3C,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,WAAW,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC9D,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;YACvB,oEAAoE;YACpE,qEAAqE;YACrE,kBAAkB;YAClB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,QAAQ,IAAI,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;QACxE,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,WAAW;IAC5D,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE;QACZ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC;QACzD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACjF,OAAO,KAAK,CAAC;IACf,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,IAAc;IAClE,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC5C,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,YAAY,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,+DAA+D;YAClG,wGAAwG;YACxG,qGAAqG;YACrG,0EAA0E,CAC7E,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// This agent's own domain knowledge of which appq tools it may touch — the
|
|
2
|
+
// enforcement mechanism (assertToolAllowed / the gated dispatcher) lives in
|
|
3
|
+
// @appliqation/agent-core, shared with every sibling agent; only the
|
|
4
|
+
// allowlist content is local. Zero write tools — genuinely absent, not
|
|
5
|
+
// gated behind a flag. This agent never calls an appq write tool and never
|
|
6
|
+
// performs a git operation; it patches local files only, and a separate
|
|
7
|
+
// agent (appliqation-pr-raise) is responsible for reviewing/committing/
|
|
8
|
+
// pushing them.
|
|
9
|
+
export const READONLY_CONTEXT_TOOLS = new Set([
|
|
10
|
+
// The test case's own expected_result/steps — the ground truth a healed
|
|
11
|
+
// selector's semantic identity gets checked against.
|
|
12
|
+
'get_scenario',
|
|
13
|
+
// If the caller knows a defect linked to this failure, its evidence
|
|
14
|
+
// (routes_visited, console/network errors) is useful diagnostic context —
|
|
15
|
+
// optional, this agent works fine without it.
|
|
16
|
+
'get_defect_context',
|
|
17
|
+
]);
|
|
18
|
+
//# sourceMappingURL=safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/tools/safety.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,4EAA4E;AAC5E,qEAAqE;AACrE,uEAAuE;AACvE,2EAA2E;AAC3E,wEAAwE;AACxE,wEAAwE;AACxE,gBAAgB;AAEhB,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC;IAC5C,wEAAwE;IACxE,qDAAqD;IACrD,cAAc;IACd,oEAAoE;IACpE,0EAA0E;IAC1E,8CAA8C;IAC9C,oBAAoB;CACrB,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appliqation/heal-selector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Standalone agent that heals one broken Playwright selector in an existing script — narrow and token-efficient, never a full regenerate. Verifies a healed selector targets the same semantic element (accessibility role + name, the test case's own expected_result) before touching anything; declines rather than guesses.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/appliqation/heal-selector.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/appliqation/heal-selector#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/appliqation/heal-selector/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"appliqation-heal-selector": "./dist/cli/index.js"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"dev": "tsx src/cli/index.ts",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"lint": "eslint src --ext .ts",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@appliqation/agent-core": "^0.1.5",
|
|
36
|
+
"commander": "^13.1.0",
|
|
37
|
+
"dotenv": "^16.4.7",
|
|
38
|
+
"playwright": "^1.51.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@eslint/js": "^9.39.5",
|
|
42
|
+
"@types/node": "^22.13.10",
|
|
43
|
+
"eslint": "^9.39.5",
|
|
44
|
+
"tsx": "^4.19.3",
|
|
45
|
+
"typescript": "^5.8.2",
|
|
46
|
+
"typescript-eslint": "^8.67.0",
|
|
47
|
+
"vitest": "^3.2.7"
|
|
48
|
+
}
|
|
49
|
+
}
|