@asrunjs/asrun 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/explore-case.md +11 -6
- package/.claude/commands/gen-spec.md +10 -6
- package/.claude/commands/heal-spec.md +13 -9
- package/.claude/commands/make-case.md +1 -1
- package/.claude/skills/exploring-cases/SKILL.md +84 -63
- package/.claude/skills/exploring-cases/references/assertion-targets.md +8 -4
- package/.claude/skills/exploring-cases/references/case-rules.md +110 -0
- package/.claude/skills/exploring-cases/references/handoff-card.js +73 -11
- package/.claude/skills/exploring-cases/references/notes.md +87 -0
- package/.claude/skills/exploring-cases/references/open-questions.md +58 -6
- package/.claude/skills/exploring-cases/references/readiness.md +7 -5
- package/.claude/skills/exploring-cases/references/tools.md +151 -0
- package/.claude/skills/exploring-cases/references/user-handoff.md +47 -18
- package/.claude/skills/generating-specs/SKILL.md +99 -54
- package/.claude/skills/generating-specs/references/conventions.md +4 -0
- package/.claude/skills/making-cases/SKILL.md +14 -7
- package/.claude/skills/making-cases/references/gaps.md +7 -4
- package/.claude/skills/making-cases/references/normalizing.md +1 -1
- package/AGENTS.md +61 -20
- package/README.md +10 -8
- package/bin/asrun.mjs +2 -0
- package/dist/lib/case-doc.d.ts +41 -4
- package/dist/lib/case-doc.js +109 -17
- package/dist/lib/explore-notes.d.ts +74 -0
- package/dist/lib/explore-notes.js +242 -0
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.js +2 -0
- package/dist/scripts/check-cases.js +89 -1
- package/dist/scripts/cli.js +9 -2
- package/dist/scripts/doctor.js +50 -3
- package/dist/scripts/explore-notes.d.ts +1 -0
- package/dist/scripts/explore-notes.js +131 -0
- package/dist/scripts/markers-confirm.d.ts +1 -0
- package/dist/scripts/markers-confirm.js +139 -0
- package/dist/ui/server/actions.js +50 -16
- package/dist/ui/server/app.js +7 -2
- package/dist/ui/server/job-store.d.ts +60 -0
- package/dist/ui/server/job-store.js +198 -0
- package/dist/ui/server/jobs.d.ts +46 -4
- package/dist/ui/server/jobs.js +361 -59
- package/dist/ui/server/process-info.d.ts +17 -0
- package/dist/ui/server/process-info.js +126 -0
- package/dist/ui/server/result-state.d.ts +10 -0
- package/dist/ui/server/result-state.js +106 -0
- package/dist/ui/shared/ai-commands.d.ts +4 -1
- package/dist/ui/shared/ai-commands.js +14 -4
- package/dist/ui/shared/types.d.ts +17 -2
- package/docs/agents/case-format.md +40 -5
- package/docs/agents/framework-dev.md +4 -0
- package/docs/ai-workflow.md +56 -16
- package/docs/cases.md +32 -8
- package/docs/commands.md +17 -1
- package/docs/theme-guide.html +56 -0
- package/docs/troubleshooting.md +40 -0
- package/docs/web-ui.md +59 -15
- package/package.json +3 -1
- package/templates/.claude/commands/explore-case.md +11 -6
- package/templates/.claude/commands/gen-spec.md +10 -5
- package/templates/.claude/commands/heal-spec.md +11 -7
- package/templates/.claude/commands/make-case.md +1 -1
- package/templates/AGENTS.md +9 -6
- package/templates/cases/_template.md +7 -0
- package/templates/package.json +2 -0
- package/ui/dist/assets/{dist-WvvCZeeu.js → dist--BXJInTq.js} +1 -1
- package/ui/dist/assets/{dist-B7HeglDd.js → dist-BMmNFIUH.js} +1 -1
- package/ui/dist/assets/{dist-DevASegc.js → dist-BXNaYHeh.js} +1 -1
- package/ui/dist/assets/{dist-Bb36rNUw.js → dist-ByLbEOlF.js} +1 -1
- package/ui/dist/assets/{dist-Dqc_tfzC.js → dist-C9ra6_BD.js} +1 -1
- package/ui/dist/assets/{dist-DfBpjgZT.js → dist-CK8mow8u.js} +1 -1
- package/ui/dist/assets/{dist-G9KNeh6u.js → dist-CQS0So-i.js} +1 -1
- package/ui/dist/assets/{dist-GvRUDL-2.js → dist-CTrwRc3Q.js} +1 -1
- package/ui/dist/assets/{dist-B9_GXR2G.js → dist-DTYj2vUD.js} +1 -1
- package/ui/dist/assets/{dist-Dq4Vnw5N.js → dist-mV0_nQZe.js} +1 -1
- package/ui/dist/assets/index-BzMqkmQ9.js +348 -0
- package/ui/dist/assets/index-DcJR-8Mt.css +2 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BJ5D2mFK.js +0 -346
- package/ui/dist/assets/index-BXY-eKBn.css +0 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
/** 100-ns ticks between 1601-01-01 and 1970-01-01, the epoch a Windows file time counts from. */
|
|
3
|
+
const WINDOWS_EPOCH_MS = 11644473600000;
|
|
4
|
+
/** True when the pid exists and this process may signal it. */
|
|
5
|
+
function alive(pid) {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(pid, 0);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
// A process of another user still exists; only a missing one is gone.
|
|
12
|
+
return error.code === 'EPERM';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What the system knows about these pids on Windows, from one PowerShell call for all of them. The command line and the
|
|
17
|
+
* creation time are what tell a leftover job from a new process that merely reused its pid.
|
|
18
|
+
*/
|
|
19
|
+
function windowsDetails(pids) {
|
|
20
|
+
const map = new Map();
|
|
21
|
+
const filter = pids.map((pid) => `ProcessId=${pid}`).join(' OR ');
|
|
22
|
+
// `\u0001` is an ordinary separator: a Windows command line never holds it.
|
|
23
|
+
const script = `$p = Get-CimInstance Win32_Process -Filter "${filter}"; ` +
|
|
24
|
+
'foreach ($x in $p) { $ft = 0; try { $ft = $x.CreationDate.ToFileTimeUtc() } catch {}; ' +
|
|
25
|
+
'Write-Output ("{0}\u0001{1}\u0001{2}" -f $x.ProcessId, $ft, ($x.CommandLine -replace "\\r?\\n", " ")) }';
|
|
26
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
timeout: 15_000,
|
|
30
|
+
});
|
|
31
|
+
if (result.status === 0 && typeof result.stdout === 'string') {
|
|
32
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
33
|
+
if (line.trim() === '')
|
|
34
|
+
continue;
|
|
35
|
+
const [pidText, fileTimeText, ...rest] = line.split('\u0001');
|
|
36
|
+
const pid = Number(pidText);
|
|
37
|
+
if (!Number.isInteger(pid))
|
|
38
|
+
continue;
|
|
39
|
+
const fileTime = Number(fileTimeText);
|
|
40
|
+
const startedAt = Number.isFinite(fileTime) && fileTime > 0 ? fileTime / 10_000 - WINDOWS_EPOCH_MS : null;
|
|
41
|
+
map.set(pid, { commandLine: rest.join('\u0001'), startedAt });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const pid of pids) {
|
|
45
|
+
if (!map.has(pid))
|
|
46
|
+
map.set(pid, alive(pid) ? { commandLine: '', startedAt: null } : null);
|
|
47
|
+
}
|
|
48
|
+
return map;
|
|
49
|
+
}
|
|
50
|
+
/** `etime` of `ps`: `MM:SS`, `HH:MM:SS` or `D-HH:MM:SS`. */
|
|
51
|
+
function parseElapsed(text) {
|
|
52
|
+
const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(text);
|
|
53
|
+
if (!match)
|
|
54
|
+
return null;
|
|
55
|
+
const days = Number(match[1] ?? 0);
|
|
56
|
+
const hours = Number(match[2] ?? 0);
|
|
57
|
+
const minutes = Number(match[3]);
|
|
58
|
+
const seconds = Number(match[4]);
|
|
59
|
+
return ((days * 24 + hours) * 60 + minutes) * 60 * 1000 + seconds * 1000;
|
|
60
|
+
}
|
|
61
|
+
/** The same answer on Linux and macOS, from one `ps` call. */
|
|
62
|
+
function posixDetails(pids) {
|
|
63
|
+
const map = new Map();
|
|
64
|
+
const result = spawnSync('ps', ['-ww', '-p', pids.join(','), '-o', 'pid=,etime=,args='], {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
timeout: 15_000,
|
|
67
|
+
});
|
|
68
|
+
if (result.status === 0 && typeof result.stdout === 'string') {
|
|
69
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
70
|
+
const match = /^\s*(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
71
|
+
if (!match)
|
|
72
|
+
continue;
|
|
73
|
+
const pid = Number(match[1]);
|
|
74
|
+
const elapsed = parseElapsed(match[2]);
|
|
75
|
+
map.set(pid, { commandLine: match[3], startedAt: elapsed === null ? null : Date.now() - elapsed });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const pid of pids) {
|
|
79
|
+
if (!map.has(pid))
|
|
80
|
+
map.set(pid, alive(pid) ? { commandLine: '', startedAt: null } : null);
|
|
81
|
+
}
|
|
82
|
+
return map;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The details of these pids, or `null` for one that is gone. One call for all of them: a restart usually has only a
|
|
86
|
+
* handful of jobs to look at, but probing should not be started once per job.
|
|
87
|
+
*/
|
|
88
|
+
export function probeProcesses(pids) {
|
|
89
|
+
const unique = [...new Set(pids.filter((pid) => Number.isInteger(pid) && pid > 0))];
|
|
90
|
+
if (unique.length === 0)
|
|
91
|
+
return new Map();
|
|
92
|
+
try {
|
|
93
|
+
return process.platform === 'win32' ? windowsDetails(unique) : posixDetails(unique);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
const fallback = new Map();
|
|
97
|
+
for (const pid of unique)
|
|
98
|
+
fallback.set(pid, alive(pid) ? { commandLine: '', startedAt: null } : null);
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Ends a process and everything it started: the agent CLI, the browser and the MCP server below it. Off Windows the
|
|
104
|
+
* process was spawned as the leader of its own group, so the whole group is signalled; Windows has no process groups,
|
|
105
|
+
* and `taskkill /T` walks the tree instead.
|
|
106
|
+
*/
|
|
107
|
+
export function killProcessTree(pid, signal) {
|
|
108
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
109
|
+
return;
|
|
110
|
+
if (process.platform === 'win32') {
|
|
111
|
+
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
process.kill(-pid, signal);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Not a group leader any more (or already gone): end what is left of the process itself.
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, signal);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Already gone.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ResultState } from '../shared/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a finished AI run declared, or null when its text holds no `RESULT:` line at all.
|
|
4
|
+
*
|
|
5
|
+
* A fence normally means "this is quoted", so its content is not the agent's own conclusion. A model still wraps its
|
|
6
|
+
* closing line in a block of its own often enough to matter, so a fence is read in two narrow cases, after nothing
|
|
7
|
+
* outside one was found: a fence that holds nothing but the result (the usual drift), and the last line of the whole
|
|
8
|
+
* message even when a fence wraps all of it (the agent answered from inside one block).
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseResultState(text: string): ResultState | null;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
2
|
+
/** Zero-width characters that a copied line may carry, which would hide the token from a line-anchored match. */
|
|
3
|
+
const ZERO_WIDTH = /[\u200b-\u200d\ufeff]/g;
|
|
4
|
+
/** The first line of a fenced block: a run of three or more backticks or tildes, then an info string without that run. */
|
|
5
|
+
const FENCE_OPEN = /^\s{0,3}(`{3,}|~{3,})([^`~]*)$/;
|
|
6
|
+
/** The last line of a fenced block: a run at least as long as the one that opened it, and nothing else. */
|
|
7
|
+
const FENCE_CLOSE = /^\s{0,3}(`{3,}|~{3,})\s*$/;
|
|
8
|
+
/** Splits the text into lines and marks the ones a fence wraps, so quoted content does not read as the conclusion. */
|
|
9
|
+
function textLines(source) {
|
|
10
|
+
const lines = [];
|
|
11
|
+
let open = null;
|
|
12
|
+
for (const text of source.split('\n')) {
|
|
13
|
+
if (open === null) {
|
|
14
|
+
const start = FENCE_OPEN.exec(text);
|
|
15
|
+
if (start) {
|
|
16
|
+
open = start[1];
|
|
17
|
+
lines.push({ text, fenced: true, marker: start[1] });
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
lines.push({ text, fenced: false });
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const end = FENCE_CLOSE.exec(text);
|
|
24
|
+
const closes = !!end && end[1][0] === open[0] && end[1].length >= open.length;
|
|
25
|
+
lines.push({ text, fenced: true, ...(closes && { marker: end[1] }) });
|
|
26
|
+
if (closes)
|
|
27
|
+
open = null;
|
|
28
|
+
}
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The last `RESULT: ready|needs-input` line of what the agent said. The token is fixed (`AGENTS.md`, "Fixed tokens"),
|
|
33
|
+
* but a model drifts in every way that still means the same thing: a short label in front of it ("结果:", "Status:",
|
|
34
|
+
* "结果 —"), a full-width colon, bold or a code span, a list bullet or a heading, an emoji, or `needs input` for
|
|
35
|
+
* `needs-input`. A run that said `needs-input` must not be read as a run that said nothing, so the format is read
|
|
36
|
+
* leniently; the line stays anchored and the value has to follow the separator at once, so ordinary prose that merely
|
|
37
|
+
* mentions the token is still not a result. The label may not run across a newline and may not hold a colon of its own.
|
|
38
|
+
*/
|
|
39
|
+
const RESULT_LINE = /^[\s\p{P}\p{S}\d]*(?:[^\n::]{1,40}(?:[::]|\s[–—-]\s)\s*)?RESULT[\s*_`]*(?:[::][\s*_`\p{S}]*|[\s*_`]*[–—-][\s*_`\p{S}]*)[\s*_`\p{S}]*(ready|needs[\s_-]?input)\b/gimu;
|
|
40
|
+
/** The state a single line declares, or null when the line is not a result line. */
|
|
41
|
+
function matchLine(text) {
|
|
42
|
+
const value = [...text.matchAll(RESULT_LINE)].at(-1)?.[1];
|
|
43
|
+
if (!value)
|
|
44
|
+
return null;
|
|
45
|
+
// The token is fixed, but a drifted `NEEDS INPUT` or `needs_input` means the same as `needs-input`.
|
|
46
|
+
return value.toLowerCase().replace(/[\s_]+/g, '-');
|
|
47
|
+
}
|
|
48
|
+
/** The state of the last of these lines that is a result line, or null when none is. */
|
|
49
|
+
function lastOf(lines) {
|
|
50
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
51
|
+
const state = matchLine(lines[index].text);
|
|
52
|
+
if (state)
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What a finished AI run declared, or null when its text holds no `RESULT:` line at all.
|
|
59
|
+
*
|
|
60
|
+
* A fence normally means "this is quoted", so its content is not the agent's own conclusion. A model still wraps its
|
|
61
|
+
* closing line in a block of its own often enough to matter, so a fence is read in two narrow cases, after nothing
|
|
62
|
+
* outside one was found: a fence that holds nothing but the result (the usual drift), and the last line of the whole
|
|
63
|
+
* message even when a fence wraps all of it (the agent answered from inside one block).
|
|
64
|
+
*/
|
|
65
|
+
export function parseResultState(text) {
|
|
66
|
+
const lines = textLines(text.replace(ANSI, '').replace(ZERO_WIDTH, ''));
|
|
67
|
+
// 1. What the agent said outside any fence: the usual case, and the only place two results can stand against each
|
|
68
|
+
// other.
|
|
69
|
+
const plain = lastOf(lines.filter((line) => !line.fenced));
|
|
70
|
+
if (plain)
|
|
71
|
+
return plain;
|
|
72
|
+
// 2. A fence whose body is the result and nothing else, as one block per fence.
|
|
73
|
+
const blocks = [];
|
|
74
|
+
let block = null;
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (!line.fenced) {
|
|
77
|
+
block = null;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!block) {
|
|
81
|
+
block = [];
|
|
82
|
+
blocks.push(block);
|
|
83
|
+
}
|
|
84
|
+
block.push(line);
|
|
85
|
+
}
|
|
86
|
+
for (let index = blocks.length - 1; index >= 0; index -= 1) {
|
|
87
|
+
const body = blocks[index].filter((line) => !line.marker && line.text.trim() !== '');
|
|
88
|
+
if (body.length !== 1)
|
|
89
|
+
continue;
|
|
90
|
+
const state = matchLine(body[0].text);
|
|
91
|
+
if (state)
|
|
92
|
+
return state;
|
|
93
|
+
}
|
|
94
|
+
// 3. The last line of the message, fence or not: the line a person reads as the conclusion, whatever wraps it.
|
|
95
|
+
// A fence that names two results is a quote of a document, not an answer, so it is not read from its tail.
|
|
96
|
+
const tail = [...lines].reverse().find((line) => !line.marker && line.text.trim() !== '');
|
|
97
|
+
if (!tail)
|
|
98
|
+
return null;
|
|
99
|
+
const tailState = matchLine(tail.text);
|
|
100
|
+
if (!tailState)
|
|
101
|
+
return null;
|
|
102
|
+
if (!tail.fenced)
|
|
103
|
+
return tailState;
|
|
104
|
+
const body = blocks.find((block) => block.includes(tail))?.filter((line) => !line.marker) ?? [];
|
|
105
|
+
return body.filter((line) => matchLine(line.text)).length === 1 ? tailState : null;
|
|
106
|
+
}
|
|
@@ -37,7 +37,10 @@ export interface AiRunOptions {
|
|
|
37
37
|
* (all that are logged in, for heal). `gen-spec` takes none: it uses every profile the case names.
|
|
38
38
|
*/
|
|
39
39
|
profiles?: string[];
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Every command: file (relative to the project root) with what the user knows, wants checked or wants changed. A
|
|
42
|
+
* reusable instruction is written into the case's `## Rules`; a one-off clue stays in the report.
|
|
43
|
+
*/
|
|
41
44
|
hints?: string;
|
|
42
45
|
/** `heal-spec` only: the report (folder name under reports/) to read first instead of the newest one. */
|
|
43
46
|
report?: string;
|
|
@@ -30,7 +30,7 @@ export const slashCommand = (command, caseId, options) => {
|
|
|
30
30
|
`${attended && command === 'explore-case' ? ' --attended' : ''}` +
|
|
31
31
|
`${picksProfiles && profiles?.length ? ` --profiles ${profiles.join(',')}` : ''}` +
|
|
32
32
|
`${heal && report ? ` --report ${report}` : ''}` +
|
|
33
|
-
`${
|
|
33
|
+
`${hints ? ` --hints ${hints}` : ''}`);
|
|
34
34
|
};
|
|
35
35
|
/** The MCP tools of the Playwright server that agents explore with. */
|
|
36
36
|
const PLAYWRIGHT_MCP = 'mcp__playwright-test__*';
|
|
@@ -52,6 +52,7 @@ function claudeAllowedTools(writes) {
|
|
|
52
52
|
'Edit(/tests/**)',
|
|
53
53
|
'Bash(pnpm typecheck:*)',
|
|
54
54
|
'Bash(pnpm spec:lint:*)',
|
|
55
|
+
'Bash(pnpm markers:confirm:*)',
|
|
55
56
|
nodeTsx,
|
|
56
57
|
'Bash(pnpm baselines:review:*)',
|
|
57
58
|
];
|
|
@@ -111,12 +112,20 @@ function invocation(tool, prompt, writes, session, model, thinking) {
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
const commandLine = ({ file, args }) => [file, ...args.map((arg) => (/^[\w@=,.:/-]+$/.test(arg) ? arg : JSON.stringify(arg)))].join(' ');
|
|
115
|
+
/**
|
|
116
|
+
* The closing line of every one-shot run. The server reads the last `RESULT:` line to tell a finished run from one that
|
|
117
|
+
* said nothing, and the shapes it drifts into are Markdown ones: a code fence, bold, a heading, a bullet. The wording is
|
|
118
|
+
* the same in every prompt so a continuation of a run asks for the same thing.
|
|
119
|
+
*/
|
|
120
|
+
const RESULT_LINE_RULE = 'The last line of your final message is plain text and nothing else: exactly "RESULT: ready" or ' +
|
|
121
|
+
'"RESULT: needs-input". Never put it in a code fence or inline code, never make it bold, a heading or a list item, ' +
|
|
122
|
+
'put no label, number or emoji in front of it, and write nothing after it.';
|
|
114
123
|
/**
|
|
115
124
|
* Said in every one-shot run: nobody is there to answer, so questions go into the case file and the last line
|
|
116
125
|
* is a result the UI can read. The skills define the details.
|
|
117
126
|
*/
|
|
118
127
|
const NON_INTERACTIVE = 'Non-interactive run: nobody can answer questions, so do not stop to ask. Write open questions into the case file ' +
|
|
119
|
-
|
|
128
|
+
`as the skill describes. ${RESULT_LINE_RULE}`;
|
|
120
129
|
/**
|
|
121
130
|
* An attended exploration: nobody answers questions in the chat, but a person sits at the machine and works in the
|
|
122
131
|
* browser window, so the agent hands them the steps that write data and waits, as user-handoff.md describes.
|
|
@@ -126,7 +135,7 @@ const ATTENDED = 'Attended run: a person is at the machine and the browser windo
|
|
|
126
135
|
'with its own call that sets __handoffCfg, before the call that runs handoff-card.js, and never change those options ' +
|
|
127
136
|
'inside the call that runs the card: the web UI sees that a person is needed only from that call, so without it ' +
|
|
128
137
|
'there is no "Your turn" badge and no notification. Nobody can answer questions in the chat, so write open ' +
|
|
129
|
-
|
|
138
|
+
`questions into the case file as the skill describes. ${RESULT_LINE_RULE}`;
|
|
130
139
|
/** The prompt of a one-shot run. Every command may leave open questions in the case, so every one ends with a RESULT line. */
|
|
131
140
|
export function headlessPrompt(command, caseId, options) {
|
|
132
141
|
const slash = slashCommand(command, caseId, options);
|
|
@@ -159,7 +168,7 @@ export function replyPrompt(command, replyFile) {
|
|
|
159
168
|
`Their answers are in ${replyFile}, relative to the project root: read that file first.\n` +
|
|
160
169
|
'Fold every answer into the case file as the skill describes: write the answer into the sentence above the ' +
|
|
161
170
|
'TODO block and remove the block, and keep a TODO block (with an empty Answer: line, emoji prefixes included) for what is still unknown. ' +
|
|
162
|
-
`Then finish the task of /${command}
|
|
171
|
+
`Then finish the task of /${command}. ${RESULT_LINE_RULE}`);
|
|
163
172
|
}
|
|
164
173
|
/** The next turn of an earlier run, in its session and with exactly the permissions of the first turn. */
|
|
165
174
|
export function continueInvocation(tool, command, replyFile, sessionId, model, thinking) {
|
|
@@ -174,6 +183,7 @@ export const makeCasePrompt = (memoPath) => `/make-case ${memoPath} ` +
|
|
|
174
183
|
'Non-interactive run: nobody can answer questions, so do not stop to ask. ' +
|
|
175
184
|
'Do not open a browser or explore pages. Write the suggested default of every gap as a normal sentence and put a ' +
|
|
176
185
|
'TODO block with an empty Answer: line under it, as open-questions.md describes (every marker starts with its emoji). ' +
|
|
186
|
+
'Never put a marker word inside a TODO block: one claim carries one signal. ' +
|
|
177
187
|
'Never overwrite an existing case: choose a new id instead. ' +
|
|
178
188
|
'Put the cases of one area in a group folder (cases/<group>/<name>.md, id <group>/<name>), reusing an existing folder when it fits. ' +
|
|
179
189
|
'Write the cases, run pnpm case:check, and list the open questions in your final report.';
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { ResultStatus } from '../../lib/run-summary.js';
|
|
2
2
|
import type { AiCommand, AiTool } from './ai-commands.js';
|
|
3
|
-
export type JobStatus = 'running' | 'succeeded' | 'failed' | 'cancelled';
|
|
3
|
+
export type JobStatus = 'running' | 'succeeded' | 'failed' | 'cancelled' | 'interrupted';
|
|
4
4
|
export type JobKind = 'signin' | 'visit' | 'run' | 'ai';
|
|
5
|
+
/**
|
|
6
|
+
* A job that was still running when the UI server stopped, and the process left behind. `alive`: it was identified as
|
|
7
|
+
* this job's process; `unknown`: a process has that pid but could not be identified, so it is never killed; `gone` /
|
|
8
|
+
* `stopped`: nothing is left to stop.
|
|
9
|
+
*/
|
|
10
|
+
export interface LeftoverInfo {
|
|
11
|
+
pid: number;
|
|
12
|
+
state: 'alive' | 'unknown' | 'gone' | 'stopped';
|
|
13
|
+
}
|
|
5
14
|
/** What changed on disk: case files (`cases/**`) and/or specs (`tests/**`). */
|
|
6
15
|
export interface FilesChange {
|
|
7
16
|
cases: boolean;
|
|
@@ -41,6 +50,12 @@ export interface JobInfo {
|
|
|
41
50
|
continuesJobId?: string;
|
|
42
51
|
/** An AI run that has handed a step to the person in the browser window and waits for them to press Done. */
|
|
43
52
|
awaitingUser?: AwaitingUserInfo | null;
|
|
53
|
+
/** When the UI server found this job still running from a previous server. */
|
|
54
|
+
interruptedAt?: string;
|
|
55
|
+
/** A job the previous UI server left behind: what is known about its process, and whether it is still there. */
|
|
56
|
+
leftover?: LeftoverInfo;
|
|
57
|
+
/** True when "Run again" can start this job from the request it was started with. */
|
|
58
|
+
rerunable?: boolean;
|
|
44
59
|
}
|
|
45
60
|
export interface AwaitingUserInfo {
|
|
46
61
|
/** What the person is asked to do, as the agent wrote it on the card; null when unknown. */
|
|
@@ -105,7 +120,7 @@ export type StartJobRequest = {
|
|
|
105
120
|
model?: string;
|
|
106
121
|
/** `explore-case` and `heal-spec` only: the profiles to open / heal; omitted = every profile that applies. `gen-spec` takes none: it uses every profile the case names. */
|
|
107
122
|
profiles?: string[];
|
|
108
|
-
/**
|
|
123
|
+
/** Every command: what the user knows or wants changed (optional). The server stores it as a file for the agent. */
|
|
109
124
|
hints?: string;
|
|
110
125
|
/** `heal-spec` only: the report to read first (folder name under reports/); omitted = the newest one. */
|
|
111
126
|
report?: string;
|
|
@@ -5,7 +5,7 @@ spec consumes it. It holds no rules of its own: the rules are in the framework `
|
|
|
5
5
|
references listed at the end.
|
|
6
6
|
|
|
7
7
|
Read `AGENTS.md` first. If a project has `.claude/skills/<skill>.md`, that override comes after the skill; the
|
|
8
|
-
project's `AGENTS.md` §2 comes last and wins over both. `F1`–`F6` always win.
|
|
8
|
+
project's `AGENTS.md` §2 comes last and wins over both. `F1`–`F6` and `R2` always win.
|
|
9
9
|
|
|
10
10
|
## Identity and paths
|
|
11
11
|
|
|
@@ -20,15 +20,17 @@ project's `AGENTS.md` §2 comes last and wins over both. `F1`–`F6` always win.
|
|
|
20
20
|
|
|
21
21
|
The parser and the web panel match these literals. A project language never translates them:
|
|
22
22
|
|
|
23
|
-
- Section headings: `## Goal`, `## Steps`, `## Expected`, `## Values`, `## Visual checks`, `## Targets`,
|
|
24
|
-
and `## Source` when the case was made from notes.
|
|
23
|
+
- Section headings: `## Goal`, `## Rules`, `## Steps`, `## Expected`, `## Values`, `## Visual checks`, `## Targets`,
|
|
24
|
+
`## Notes`, and `## Source` when the case was made from notes.
|
|
25
25
|
- Front matter keys: `id`, `title`, `login`, `suites`, `writes`, `writes-note`, `envs`.
|
|
26
26
|
- Step tags at the end of a step line: `[input]`, `[writes]`, `[email]`.
|
|
27
27
|
- Under a step: `- Ready when:` lines and `- Recipe (phase):` lines.
|
|
28
28
|
- Target cards: `### T<n> <name>` and their references `[T<n>]`.
|
|
29
29
|
- Questions: `🚫 TODO [blocking]`, `❓ TODO [non-blocking]`, `✍️ Answer:`, and the markers
|
|
30
|
-
`🔍 to be confirmed
|
|
31
|
-
|
|
30
|
+
`🔍 to be confirmed` / `👁️ not observed`. A marker's parentheses may hold its reason (`🔍 to be confirmed: <what a
|
|
31
|
+
person must decide>`) and a screenshot (`evidence: <path under .explore/>`); `(seen on <profile>)` is a plain note.
|
|
32
|
+
- The last line of a run: `RESULT: ready` or `RESULT: needs-input`, plain text, alone on its line (no label in front of
|
|
33
|
+
it, no code fence or emphasis around it).
|
|
32
34
|
|
|
33
35
|
## Front matter
|
|
34
36
|
|
|
@@ -56,6 +58,28 @@ hard-codes tags.
|
|
|
56
58
|
|
|
57
59
|
## Sections
|
|
58
60
|
|
|
61
|
+
### Rules
|
|
62
|
+
|
|
63
|
+
Case-local instructions for the agents that explore, generate and heal this case: **how** to run it, never **what is
|
|
64
|
+
true** (framework baseline `R1`–`R4`). The section is optional and written by a person; an AI run may add a rule only
|
|
65
|
+
under `R4` and never rewrites or deletes one silently.
|
|
66
|
+
|
|
67
|
+
- One top-level bullet per rule, plain prose. No nested bullets and no code fences: `parseBullets` reads only the
|
|
68
|
+
top-level bullets, so a nested list is not part of the rule the panel edits.
|
|
69
|
+
- A bullet may start with `Step <n>:`, `Before step <n>:` or `After step <n>:` to tie it to that step.
|
|
70
|
+
`pnpm case:check` fails when that step does not exist. Without a prefix the rule applies to the whole case.
|
|
71
|
+
- Use it for a page quirk, a mandatory read-only preparation, a control to avoid, or a waiting/attention hint.
|
|
72
|
+
- Do **not** use it for an expectation (`## Expected`), a literal (`## Values`), a target (`## Targets`), or a step that
|
|
73
|
+
writes, sends email or needs a person. Those stay in their own sections, and such a step keeps its tags and recipe.
|
|
74
|
+
- A rule that conflicts with the rest of the case is a `🚫 TODO [blocking]` question, not an override. A rule that no
|
|
75
|
+
longer holds (the page changed, the banner is gone) is recorded with a TODO and evidence, never deleted silently.
|
|
76
|
+
- The same artifact language as the rest of the case applies; the heading itself stays `## Rules`.
|
|
77
|
+
|
|
78
|
+
Good: `- Before step 1: dismiss the cookie banner; it covers the header and blocks clicks.`
|
|
79
|
+
Good: `- Never use "Continue with Google": it requires 2FA and is out of scope.`
|
|
80
|
+
Wrong: `- The header logo must be the Acme logo.` (an expectation → `## Expected` / `## Values` / `## Targets`)
|
|
81
|
+
Wrong: `- Create a draft named ... and delete it at the end.` (a write → a tagged step with a recipe)
|
|
82
|
+
|
|
59
83
|
### Steps
|
|
60
84
|
|
|
61
85
|
- Numbered Markdown lines (`1.`, `2.`, ...). The spec wraps each one in `test.step('<n>. <text>', ...)` with the text
|
|
@@ -70,7 +94,11 @@ hard-codes tags.
|
|
|
70
94
|
- A question block is indented under the sentence it belongs to: 4 spaces under a numbered step, 2 under a bullet.
|
|
71
95
|
The parent line is the suggestion, written as a finished sentence; the `TODO` line holds one question and optional
|
|
72
96
|
`(evidence)`; the block ends with `- ✍️ Answer:` at one level deeper and that line is the last line of the block.
|
|
97
|
+
Hang it under the immediate sentence, never under a broader one above it, and never inside the `## Values` yaml.
|
|
73
98
|
Details: `.claude/skills/exploring-cases/references/open-questions.md`.
|
|
99
|
+
- A marker (`🔍 to be confirmed: <what a person must decide>`) is the alternative to a question, not a companion: it
|
|
100
|
+
says nobody has to decide, only to check. `(seen on <profile>)` is a plain note the run checks by itself. Its
|
|
101
|
+
`evidence: <path>` is served by the panel from `.explore/`.
|
|
74
102
|
|
|
75
103
|
### Expected
|
|
76
104
|
|
|
@@ -94,6 +122,8 @@ example-staging:
|
|
|
94
122
|
`TODO` fails with "Incomplete values ...". Fill them before running; never make a spec read the YAML itself.
|
|
95
123
|
- Values hold literals only: names, labels, options, test-data names. The checks themselves stay in `## Steps` and
|
|
96
124
|
`## Expected`, and selectors come from `## Targets`.
|
|
125
|
+
- No marker or question inside the yaml: it would become part of the expected value. Put it on the prose line above
|
|
126
|
+
the block and name the key there.
|
|
97
127
|
|
|
98
128
|
### Visual checks
|
|
99
129
|
|
|
@@ -155,6 +185,9 @@ test.describe('<Case title>', {tag: caseTags('<id>')}, () => {
|
|
|
155
185
|
- `// target: T<n>` goes on the line before an image/screenshot/PDF check. `// ready: <signal>` goes before a check
|
|
156
186
|
for absence ("missing", "empty", "hidden") and needs a positive wait before it. `// pages: <the case's words>` goes
|
|
157
187
|
before an `expectPdf` that follows a case limit written in strong words ("only", "exactly", "must").
|
|
188
|
+
- `## Rules` are applied while replaying the case and writing the spec, but they never become assertions. Code that
|
|
189
|
+
exists only because of a rule carries `// rule: <short quote>` on the line before it. A rule that cannot be honored
|
|
190
|
+
is a question in the case, never an invented value or a skipped expectation.
|
|
158
191
|
- Import `test`/`expect` from `@asrunjs/asrun/fixtures`, never from `@playwright/test`. Locators:
|
|
159
192
|
`getByTestId` > `getByRole` > `getByLabel` > text.
|
|
160
193
|
- No branching on `profile.id` or `profile.env`: differences go into `## Values`, selectors into `## Targets`.
|
|
@@ -185,9 +218,11 @@ test.describe('<Case title>', {tag: caseTags('<id>')}, () => {
|
|
|
185
218
|
|
|
186
219
|
In the framework package:
|
|
187
220
|
|
|
221
|
+
- `.claude/skills/exploring-cases/references/case-rules.md` — how `## Rules` are read, applied and maintained.
|
|
188
222
|
- `.claude/skills/exploring-cases/references/assertion-targets.md` — what a check looks at, and when to ask.
|
|
189
223
|
- `.claude/skills/exploring-cases/references/readiness.md` — wait for the content, optional confirmations, downloads.
|
|
190
224
|
- `.claude/skills/exploring-cases/references/open-questions.md` — question blocks and how answers are folded in.
|
|
191
225
|
- `.claude/skills/exploring-cases/references/user-handoff.md` — attended exploration and the handoff card.
|
|
226
|
+
- `.claude/skills/exploring-cases/references/notes.md` — the local exploration note `/gen-spec` reads (not part of the case).
|
|
192
227
|
- `.claude/skills/generating-specs/references/conventions.md` — spec shape, assertion layers, readable failures.
|
|
193
228
|
- `docs/cases.md` — the same format for people writing a case by hand.
|
|
@@ -53,6 +53,10 @@ is relevant to a project that uses asrun as a dependency.
|
|
|
53
53
|
- Everything the framework reads for a project goes through `ROOT` in `lib/paths.ts` (the workspace root); its own
|
|
54
54
|
assets (templates, docs, skills, its `node_modules`) go through `FRAMEWORK_ROOT`. Never write into
|
|
55
55
|
`FRAMEWORK_ROOT` at run time.
|
|
56
|
+
- `.explore/` is local state (git-ignored): target reference screenshots and the exploration notes that
|
|
57
|
+
`lib/explore-notes.ts` reads (`.explore/notes/<case-id>/<profile-id>.json`). Nothing there is committed, and a
|
|
58
|
+
workspace without it generates specs by replaying the case. `scripts/explore-notes.ts` (`pnpm explore:notes`) prints
|
|
59
|
+
and validates those notes.
|
|
56
60
|
- After changing `lib/`, run `pnpm build` before exercising the demo's specs.
|
|
57
61
|
- `pnpm build` copies `scripts/*.mjs` into `dist/scripts/`, where they sit one folder deeper than in this repository.
|
|
58
62
|
`pnpm verify:pack` (part of CI) checks that layout on a packed tarball; the checkout alone hides such bugs, because
|
package/docs/ai-workflow.md
CHANGED
|
@@ -8,8 +8,8 @@ so the procedure lives in one place and a project can refine it (see
|
|
|
8
8
|
| Command | What it does |
|
|
9
9
|
|---|---|
|
|
10
10
|
| `/make-case <notes-or-path>` | Turn raw notes into case documents. Optional step. |
|
|
11
|
-
| `/explore-case <case-id> <env> [--attended] [--profiles a,b]` | Open the real pages and write what they show into the case. |
|
|
12
|
-
| `/gen-spec <case-id> <env> [--force]` | Write `tests/<case-id>.spec.ts` and fill the case's `## Values`. |
|
|
11
|
+
| `/explore-case <case-id> <env> [--attended] [--profiles a,b] [--hints <file>]` | Open the real pages and write what they show into the case. |
|
|
12
|
+
| `/gen-spec <case-id> <env> [--force] [--hints <file>]` | Write `tests/<case-id>.spec.ts` and fill the case's `## Values`. |
|
|
13
13
|
| `/heal-spec <case-id> <env> [--profiles a,b] [--report <run-id>] [--hints <file>]` | Repair a failing spec, or refine a case and its spec on request, from the last report and what you asked for. |
|
|
14
14
|
|
|
15
15
|
The environment is **required** by every command except `/make-case`, which does not open pages. There is no default.
|
|
@@ -17,7 +17,13 @@ The environment is **required** by every command except `/make-case`, which does
|
|
|
17
17
|
`--profiles` exists on `/explore-case` and `/heal-spec`, not on `/gen-spec`: a spec runs once per profile in the
|
|
18
18
|
environment, so it is generated from every profile the case holds information for, never from a chosen subset.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
`--hints <file>` (or free text after the arguments) is optional on all three commands that read a case: what you know,
|
|
21
|
+
want checked or want changed. It guides that run. After the work, a reusable instruction that is **method**, not an
|
|
22
|
+
expectation, is written into the case's `## Rules`; a one-off clue stays in the report. See
|
|
23
|
+
[Rules for one case](#rules-for-one-case).
|
|
24
|
+
|
|
25
|
+
Every command ends with `RESULT: ready` or `RESULT: needs-input` as plain text on its last line (never in a code fence
|
|
26
|
+
or in emphasis: the web UI reads that line to show the outcome). `needs-input` means somebody has to answer a
|
|
21
27
|
question in the case file (or, in the web UI, in the job's **Answer** sheet). A one-shot run cannot ask in the chat:
|
|
22
28
|
it writes its questions into the case and stops.
|
|
23
29
|
|
|
@@ -49,6 +55,19 @@ It writes `cases/<id>.md` only, and never overwrites an existing case. `pnpm cas
|
|
|
49
55
|
`pnpm case:new header-logo` creates `cases/header-logo.md` from the template, or use a case you already have. The
|
|
50
56
|
format is in [cases.md](./cases.md). Write the steps, the expectations and — when you know them — the values.
|
|
51
57
|
|
|
58
|
+
### Rules for one case
|
|
59
|
+
|
|
60
|
+
A case can hold a `## Rules` section: case-local instructions on **how** to run it (dismiss a cookie banner first, wait
|
|
61
|
+
for a dialog after a request, never use an entry that needs 2FA). It is the place to talk to the agent without putting
|
|
62
|
+
a requirement into the wrong section. One bullet per rule; `Step 3:`, `Before step 3:` or `After step 3:` ties it to a
|
|
63
|
+
step, and `pnpm case:check` fails when that step does not exist.
|
|
64
|
+
|
|
65
|
+
Rules never say what the case proves, never replace a step or an expected value, and never allow writing data, sending
|
|
66
|
+
email or doing a person's step: those stay in `## Expected`, `## Values` and tagged steps with a recipe. The AI reads
|
|
67
|
+
and applies every rule in every run, re-checks them against the page, and lists them in its report. `/explore-case`,
|
|
68
|
+
`/gen-spec` and `/heal-spec` re-check the rules and never delete or rewrite one on their own; a rule that no longer
|
|
69
|
+
holds becomes a question for you.
|
|
70
|
+
|
|
52
71
|
## 2. Explore the case (only where a person and the agent work together)
|
|
53
72
|
|
|
54
73
|
```
|
|
@@ -58,11 +77,16 @@ format is in [cases.md](./cases.md). Write the steps, the expectations and — w
|
|
|
58
77
|
The agent opens the real pages of the profiles that are logged in and only **reads** them. It never creates, edits,
|
|
59
78
|
publishes, shares or deletes anything, in staging or in production. It:
|
|
60
79
|
|
|
61
|
-
- fixes wording in the case file and writes what the pages show into it
|
|
80
|
+
- fixes wording in the case file and writes what the pages show into it: a plain note `(seen on <profile>)` when the
|
|
81
|
+
run checks it, and `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)` only for a line that
|
|
82
|
+
would pass even if it were wrong;
|
|
62
83
|
- turns what it cannot know into `🚫 TODO [blocking]` / `❓ TODO [non-blocking]` questions, each under a sentence that
|
|
63
84
|
is its suggestion and can stand in the case as it is, with a last line `- ✍️ Answer:`;
|
|
64
85
|
- lists the candidates instead of guessing a target (`## Targets`);
|
|
65
|
-
- edits the case file
|
|
86
|
+
- edits the case file and the `.explore/` files (reference screenshots and the note below), and nothing else;
|
|
87
|
+
- writes a local **exploration note** (`.explore/notes/<case-id>/<profile-id>.json`, git-ignored) with the locators and
|
|
88
|
+
data-call paths it saw. `/gen-spec` reads it to verify instead of rediscovering, and a page that only exists after a
|
|
89
|
+
write step is written from it. Delete it at any time: without it, generation replays the case.
|
|
66
90
|
|
|
67
91
|
With many profiles, `--profiles a,b` opens only those (ids from `profiles.yaml`, comma-separated). The others are not
|
|
68
92
|
opened and nothing is written about them, so a case can be explored in passes: A and B first, C later. Runs add up; the
|
|
@@ -82,7 +106,9 @@ the browser window opens and the agent waits while you do those steps.
|
|
|
82
106
|
- The window shows a small draggable card in the bottom-left corner with what to do, **Done** (hand control back) and
|
|
83
107
|
**Cannot do this**. The agent also continues by itself when the finish condition it named shows up (for example a
|
|
84
108
|
"Saved" message); the card turns green when control is back. It waits up to 10 minutes per step.
|
|
85
|
-
-
|
|
109
|
+
- Recording starts and stops by itself. If Playwright's own recording toolbar shows at the top of the page, leave it
|
|
110
|
+
alone: it can stop the recording.
|
|
111
|
+
- The card records what you did, and the agent writes it into the case under the step as **general recipe lines**
|
|
86
112
|
(`- Recipe (phase): ...`): positions instead of names of changeable data, `[asrun]` names for created data, control
|
|
87
113
|
labels but no selectors.
|
|
88
114
|
- Delete the `[asrun]` items you created before you press Done on the last step.
|
|
@@ -115,6 +141,11 @@ subset adds up: explore A and B first and C later, and `/gen-spec` covers all th
|
|
|
115
141
|
about runs on `default` alone, and the report names it. A profile that is not logged in is not dropped: its part is
|
|
116
142
|
written from the case and marked `not verified on the page`.
|
|
117
143
|
|
|
144
|
+
It reads each profile's **exploration note** first: a fresh note supplies the locators and request patterns, which it
|
|
145
|
+
confirms on the page instead of rediscovering; a missing or stale note means a full replay. A page that only exists
|
|
146
|
+
after a write step is written from a fresh note. `pnpm explore:notes <case-id> [--profile <id>]` shows what was
|
|
147
|
+
recorded, and `--check` validates it.
|
|
148
|
+
|
|
118
149
|
It does **not** run the spec and does **not** create screenshot baselines: running is a separate step (6).
|
|
119
150
|
|
|
120
151
|
- A case must be in the project's artifact language. If it is in another language, `/gen-spec` translates the case
|
|
@@ -161,7 +192,9 @@ whether it found a real product problem, and it **never** edits an expectation j
|
|
|
161
192
|
instead of failing them).
|
|
162
193
|
- The web UI first opens a dialog: which profiles to run (every logged-in profile ticked) and an optional text box
|
|
163
194
|
for what should be repaired or improved, with evidence. The text is stored in `.tmp/hints/` and passed as `--hints`.
|
|
164
|
-
It guides the work; it never lowers the bar for a product finding.
|
|
195
|
+
It guides the work; it never lowers the bar for a product finding. A reusable instruction that is method rather than
|
|
196
|
+
an expectation is also written into the case's `## Rules` (see [Rules for one case](#rules-for-one-case)); a one-off
|
|
197
|
+
clue stays in the report.
|
|
165
198
|
- It reads the existing report first:
|
|
166
199
|
|
|
167
200
|
```bash
|
|
@@ -174,15 +207,20 @@ whether it found a real product problem, and it **never** edits an expectation j
|
|
|
174
207
|
performs a step that writes data. After editing the spec, it runs the case once to confirm.
|
|
175
208
|
- The case is the source of truth: a change to a step, an expectation, a value or a target is written into the case
|
|
176
209
|
first, then into the spec. A large change regenerates the spec from the case.
|
|
177
|
-
- **Review gate.** If the change leaves an open question or
|
|
178
|
-
before the spec: you review the case, answer
|
|
179
|
-
|
|
210
|
+
- **Review gate.** If the change leaves an open question or a `🔍 to be confirmed` marker in the case, healing stops
|
|
211
|
+
before the spec: you review the case, answer what it asks or settle the marker (the panel shows why it needs you, and
|
|
212
|
+
`pnpm markers:confirm` settles the ones a passing run already checks), then run `/explore-case` again. The spec is
|
|
213
|
+
generated after that, not before. A plain `(seen on <profile>)` note never stops it: the run checks those. A marker
|
|
214
|
+
inside `## Rules` does not stop the spec either, unless it covers a literal the spec asserts.
|
|
180
215
|
|
|
181
216
|
## Why a generated spec can be trusted
|
|
182
217
|
|
|
183
|
-
|
|
184
|
-
`.claude/skills/exploring-cases/references/assertion-targets.md` and `readiness.md`.
|
|
218
|
+
Several binding rules hold in exploring, generating and healing. They live in the framework's `AGENTS.md` and in
|
|
219
|
+
`.claude/skills/exploring-cases/references/case-rules.md`, `assertion-targets.md` and `readiness.md`.
|
|
185
220
|
|
|
221
|
+
- **Case rules.** A `## Rules` bullet says how to run the case, never what it proves. The agent applies it while
|
|
222
|
+
exploring and generating, marks code that exists only because of a rule with `// rule: <short quote>`, and lists every
|
|
223
|
+
rule in its report. It never becomes an assertion and never authorizes a write or a person's step.
|
|
186
224
|
- **Targets.** A check on a logo, image, icon, file, PDF, screenshot or repeated text points to a `[T1]` card under
|
|
187
225
|
`## Targets`: what it is (and what it is not), where it is, how it is identified (never `.first()` or a bare `img`),
|
|
188
226
|
when it is ready, and a highlighted reference screenshot under `.explore/targets/`. When in doubt the agent does not
|
|
@@ -204,7 +242,8 @@ Checks you can run yourself:
|
|
|
204
242
|
|---|---|
|
|
205
243
|
| `pnpm spec:lint [case-id]` | fails on screenshots without a target, `.first()` on images, sleeps, `networkidle`, raw download events, a `%PDF` check on its own |
|
|
206
244
|
| `pnpm baselines:review [case-id] [--new]` | lists every baseline with the card that says what it must show, so you can look at each new image |
|
|
207
|
-
| `pnpm case:check [case-id]` | fails on an incomplete card or
|
|
245
|
+
| `pnpm case:check [case-id]` | fails on an incomplete card, bad values, a marker inside a question or a question under a marked line; warns on an image check with no card and on a marker that does not say what to decide |
|
|
246
|
+
| `pnpm markers:confirm --case <id> --env <env> [--report <run-id>]` | after a passing run, removes the `🔍 to be confirmed` markers whose line the spec checks with `// unconfirmed:`; `--dry-run` lists them |
|
|
208
247
|
| `pnpm pdf:render <file.pdf> --out <dir> [--pages all] [--corner top-left]` | renders the pages to images and prints the text, to look at a PDF by hand |
|
|
209
248
|
|
|
210
249
|
## Customizing the prompts
|
|
@@ -225,11 +264,12 @@ A project refines a skill with a **flat override file**, named after the skill (
|
|
|
225
264
|
The command and the skill read it; it is not discovered as a standalone skill. Rules combine in this order, and a
|
|
226
265
|
later one wins where they conflict:
|
|
227
266
|
|
|
228
|
-
1. `F1`–`F6` of the framework baseline: safety, in every project. Nothing can change them.
|
|
267
|
+
1. `F1`–`F6` and `R2` of the framework baseline: safety, in every project. Nothing can change them.
|
|
229
268
|
2. The framework baseline, `<framework>/AGENTS.md`.
|
|
230
269
|
3. The framework skill.
|
|
231
270
|
4. The project override `.claude/skills/<skill>.md`.
|
|
232
271
|
5. The project's own `AGENTS.md` §2 (Project rules).
|
|
272
|
+
6. A case's `## Rules`: how to run that one case, never what is true.
|
|
233
273
|
|
|
234
274
|
An override is plain Markdown; write only what differs:
|
|
235
275
|
|
|
@@ -241,8 +281,8 @@ An override is plain Markdown; write only what differs:
|
|
|
241
281
|
- The staging account has no published documents: assert the empty state there instead.
|
|
242
282
|
```
|
|
243
283
|
|
|
244
|
-
The project's `AGENTS.md` §2 is the other place: a table of rule ids (`F7`–`F10`; `F1`–`F6`
|
|
245
|
-
(stricter) or `replaces` (a different default). For example, a project that writes cases in Simplified Chinese
|
|
284
|
+
The project's `AGENTS.md` §2 is the other place: a table of rule ids (`F7`–`F10`, `R1`, `R3`, `R4`; `F1`–`F6` and
|
|
285
|
+
`R2` are fixed) with `narrows` (stricter) or `replaces` (a different default). For example, a project that writes cases in Simplified Chinese
|
|
246
286
|
`replaces` `F10`. `pnpm asrun doctor` fails on an unknown id, a missing `Effect`, or a non-negotiable rule, and lists
|
|
247
287
|
the overrides it sees.
|
|
248
288
|
|