@asrunjs/asrun 0.13.0 → 0.15.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 +8 -3
- package/.claude/skills/exploring-cases/SKILL.md +72 -14
- package/.claude/skills/exploring-cases/references/handoff-card.js +81 -7
- package/.claude/skills/exploring-cases/references/notes.md +42 -4
- package/.claude/skills/exploring-cases/references/open-questions.md +2 -2
- package/.claude/skills/exploring-cases/references/readiness.md +6 -0
- package/.claude/skills/exploring-cases/references/user-handoff.md +60 -27
- package/.claude/skills/generating-specs/SKILL.md +10 -3
- package/AGENTS.md +12 -4
- package/README.md +14 -2
- package/dist/lib/desktop-notify.d.ts +38 -0
- package/dist/lib/desktop-notify.js +158 -0
- package/dist/lib/explore-notes.d.ts +15 -0
- package/dist/lib/explore-notes.js +34 -1
- package/dist/lib/migrations.d.ts +43 -4
- package/dist/lib/migrations.js +39 -7
- package/dist/lib/paths.d.ts +9 -2
- package/dist/lib/paths.js +32 -5
- package/dist/lib/pdf.js +23 -3
- package/dist/lib/scaffold.d.ts +87 -0
- package/dist/lib/scaffold.js +220 -0
- package/dist/lib/term.d.ts +36 -0
- package/dist/lib/term.js +77 -0
- package/dist/lib/update-check.d.ts +1 -7
- package/dist/lib/update-check.js +3 -15
- package/dist/lib/upgrade-prompt.d.ts +84 -0
- package/dist/lib/upgrade-prompt.js +233 -0
- package/dist/migrations/index.d.ts +12 -4
- package/dist/migrations/index.js +40 -5
- package/dist/migrations/manifest.json +27 -1
- package/dist/scripts/build.js +4 -1
- package/dist/scripts/check-cases.js +3 -3
- package/dist/scripts/cli.js +11 -3
- package/dist/scripts/doctor.js +73 -4
- package/dist/scripts/explore-notes.js +19 -2
- package/dist/scripts/init.d.ts +6 -26
- package/dist/scripts/init.js +82 -101
- package/dist/scripts/migrate.js +84 -21
- package/dist/scripts/migrations-check.d.ts +1 -0
- package/dist/scripts/migrations-check.js +272 -0
- package/dist/scripts/migrations-freeze.d.ts +1 -0
- package/dist/scripts/migrations-freeze.js +126 -0
- package/dist/scripts/notify-test.d.ts +1 -0
- package/dist/scripts/notify-test.js +48 -0
- package/dist/scripts/upgrade.js +53 -10
- package/dist/ui/server/actions.js +4 -2
- package/dist/ui/server/ai-stream.d.ts +7 -1
- package/dist/ui/server/ai-stream.js +26 -7
- package/dist/ui/server/app.js +4 -0
- package/dist/ui/server/jobs.d.ts +10 -0
- package/dist/ui/server/jobs.js +41 -1
- package/dist/ui/shared/ai-commands.js +4 -2
- package/dist/ui/shared/types.d.ts +8 -0
- package/docs/agents/case-format.md +8 -1
- package/docs/agents/framework-dev.md +44 -5
- package/docs/ai-workflow.md +27 -10
- package/docs/cases.md +5 -5
- package/docs/commands.md +73 -15
- package/docs/install.md +18 -5
- package/docs/troubleshooting.md +17 -0
- package/docs/web-ui.md +23 -8
- package/package.json +5 -2
- package/templates/.claude/commands/explore-case.md +9 -3
- package/templates/.claude/commands/upgrade-workspace.md +21 -0
- package/templates/AGENTS.md +1 -1
- package/templates/package.json +1 -1
- package/ui/dist/assets/{dist-C46EmZuI.js → dist-B4pM62Hc.js} +1 -1
- package/ui/dist/assets/{dist-DGzg2gyA.js → dist-BFADGd4p.js} +1 -1
- package/ui/dist/assets/{dist-P9roUi1O.js → dist-Bd-TmWTD.js} +1 -1
- package/ui/dist/assets/{dist-DMiOs9Qj.js → dist-Bt053MSo.js} +1 -1
- package/ui/dist/assets/{dist-DrFzCwMc.js → dist-CD9RixNE.js} +1 -1
- package/ui/dist/assets/{dist-_0g6iC9K.js → dist-ChACtLiA.js} +1 -1
- package/ui/dist/assets/{dist-D9GmSkjm.js → dist-CiW9sEPF.js} +1 -1
- package/ui/dist/assets/{dist-Db_XhDui.js → dist-CxEpzoZU.js} +1 -1
- package/ui/dist/assets/{dist-BB2cF3XN.js → dist-Dh7um7By.js} +1 -1
- package/ui/dist/assets/{dist-Dy45nXf8.js → dist-gHwKkZ6g.js} +1 -1
- package/ui/dist/assets/{index-BN4ucmmn.js → index-Ckax27uf.js} +158 -158
- package/ui/dist/index.html +1 -1
|
@@ -271,11 +271,15 @@ const BROWSER_RUN_CODE = /browser_run_code/;
|
|
|
271
271
|
* badge on.
|
|
272
272
|
*/
|
|
273
273
|
const HANDOFF_CFG_TTL_MS = 300_000;
|
|
274
|
-
/**
|
|
275
|
-
function
|
|
276
|
-
const match =
|
|
274
|
+
/** A plain string literal of the options call that comes right before the card (`message: '...'`, `finish: '...'`). */
|
|
275
|
+
function handoffText(code, key) {
|
|
276
|
+
const match = new RegExp(`(?:^|[^\\w$])${key}\\s*:\\s*(['"\`])((?:\\\\.|(?!\\1)[^\\\\])*)\\1`).exec(code);
|
|
277
277
|
return match ? match[2].replace(/\\(['"`\\])/g, '$1').trim() || null : null;
|
|
278
278
|
}
|
|
279
|
+
/** `message: '...'` of the options call that comes right before the card. */
|
|
280
|
+
const handoffMessage = (code) => handoffText(code, 'message');
|
|
281
|
+
/** `finish: '...'` of the options call that comes right before the card. */
|
|
282
|
+
const handoffFinish = (code) => handoffText(code, 'finish');
|
|
279
283
|
const codeOf = (input) => {
|
|
280
284
|
if (!input || typeof input !== 'object')
|
|
281
285
|
return '';
|
|
@@ -306,8 +310,12 @@ export class AiStreamFormatter {
|
|
|
306
310
|
pending = new Map();
|
|
307
311
|
cardCalls = new Set();
|
|
308
312
|
waitingForUser = null;
|
|
309
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* The message and finish condition of the last options call, kept so a card that runs after unrelated calls still
|
|
315
|
+
* carries them.
|
|
316
|
+
*/
|
|
310
317
|
lastHandoffMessage = null;
|
|
318
|
+
lastHandoffFinish = null;
|
|
311
319
|
lastEventAt = Date.now();
|
|
312
320
|
lastWaitAt = 0;
|
|
313
321
|
/** The last call the agent finished, so a quiet spell can say what it is doing instead of only that nothing came. */
|
|
@@ -348,18 +356,29 @@ export class AiStreamFormatter {
|
|
|
348
356
|
this.waitingForUser = null;
|
|
349
357
|
else {
|
|
350
358
|
this.cardCalls.add(id);
|
|
351
|
-
// `??=` on purpose: the options call before it already knows the message and
|
|
352
|
-
this.waitingForUser ??= {
|
|
359
|
+
// `??=` on purpose: the options call before it already knows the message, the finish condition and the start.
|
|
360
|
+
this.waitingForUser ??= {
|
|
361
|
+
message: handoffMessage(codeOf(input)) ?? this.lastHandoffMessage,
|
|
362
|
+
finish: handoffFinish(codeOf(input)) ?? this.lastHandoffFinish,
|
|
363
|
+
since: Date.now(),
|
|
364
|
+
};
|
|
353
365
|
}
|
|
354
366
|
return null;
|
|
355
367
|
}
|
|
356
368
|
if (kind === 'options') {
|
|
357
369
|
const message = handoffMessage(codeOf(input));
|
|
370
|
+
const finish = handoffFinish(codeOf(input));
|
|
358
371
|
if (message)
|
|
359
372
|
this.lastHandoffMessage = message;
|
|
373
|
+
if (finish)
|
|
374
|
+
this.lastHandoffFinish = finish;
|
|
360
375
|
// Keep the start time when the wait is already set: a second report of the same handoff must not announce it twice.
|
|
361
376
|
const since = this.waitingForUser?.since ?? Date.now();
|
|
362
|
-
this.waitingForUser = {
|
|
377
|
+
this.waitingForUser = {
|
|
378
|
+
message: message ?? this.lastHandoffMessage,
|
|
379
|
+
finish: finish ?? this.lastHandoffFinish,
|
|
380
|
+
since,
|
|
381
|
+
};
|
|
363
382
|
return null;
|
|
364
383
|
}
|
|
365
384
|
// Anything else ends the wait, unless it was set too recently to be an abandoned one: the card runs right after it.
|
package/dist/ui/server/app.js
CHANGED
|
@@ -15,6 +15,7 @@ import { JobConflictError, jobs } from './jobs.js';
|
|
|
15
15
|
import { listAiModels } from './ai-models.js';
|
|
16
16
|
import { fileEvents } from './watch-files.js';
|
|
17
17
|
import { refreshUpdate, updateEvents, updateInfo } from './update.js';
|
|
18
|
+
import { upgradePrompt } from '../../lib/upgrade-prompt.js';
|
|
18
19
|
const REPORTS_DIR = path.join(ROOT, 'reports');
|
|
19
20
|
// Built by `prepack` and shipped with the package; the framework repository builds it with `pnpm ui`.
|
|
20
21
|
const WEB_DIST = path.join(FRAMEWORK_ROOT, 'ui', 'dist');
|
|
@@ -240,6 +241,9 @@ export function createApp({ port }) {
|
|
|
240
241
|
void refreshUpdate(true);
|
|
241
242
|
return c.json(info);
|
|
242
243
|
});
|
|
244
|
+
// What an upgrade leaves open, and the prompt that hands it to an agent. Read-only; the dialog asks for it only when
|
|
245
|
+
// it opens, because it reads the installed templates and can be long.
|
|
246
|
+
api.get('/upgrade/prompt', (c) => c.json(upgradePrompt(ROOT)));
|
|
243
247
|
api.get('/reports', (c) => {
|
|
244
248
|
if (!fs.existsSync(REPORTS_DIR))
|
|
245
249
|
return c.json([]);
|
package/dist/ui/server/jobs.d.ts
CHANGED
|
@@ -102,6 +102,16 @@ declare class JobManager {
|
|
|
102
102
|
private verifyLeftoverStopped;
|
|
103
103
|
/** Publishes a change of "waiting for the person" (a handoff card) like any other status change. */
|
|
104
104
|
private syncAwaiting;
|
|
105
|
+
/**
|
|
106
|
+
* Shows a desktop notification now. Neither the agent (whose shell the CLI may refuse) nor the page (not open, not
|
|
107
|
+
* focused, not allowed) can be relied on for this, so the server shows it itself.
|
|
108
|
+
*/
|
|
109
|
+
private notifyWaiting;
|
|
110
|
+
/**
|
|
111
|
+
* Why no notification appeared, written where the person reads next: never a reason to fail the job, and the next
|
|
112
|
+
* run of the same kind leaves no line at all when the notifier works.
|
|
113
|
+
*/
|
|
114
|
+
private pushNotificationFailure;
|
|
105
115
|
/**
|
|
106
116
|
* Stops a run whose agent has said nothing for `STALL_MS`: a tool call that never returns leaves the CLI waiting for
|
|
107
117
|
* ever, and the log would only keep repeating "still waiting for ...". The block above the cancel names what to do.
|
package/dist/ui/server/jobs.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import { StringDecoder } from 'node:string_decoder';
|
|
6
6
|
import { ROOT } from '../../lib/paths.js';
|
|
7
7
|
import { spawnTarget } from '../../lib/command.js';
|
|
8
|
+
import { notifyDesktop } from '../../lib/desktop-notify.js';
|
|
8
9
|
import { AiStreamFormatter } from './ai-stream.js';
|
|
9
10
|
import { parseResultState } from './result-state.js';
|
|
10
11
|
import { appendChunkNow, chunksFile, ensureJobDir, logFile, pruneJobs, readChunks, readJobs, writeRecord, } from './job-store.js';
|
|
@@ -244,6 +245,11 @@ class JobManager {
|
|
|
244
245
|
finished = true;
|
|
245
246
|
this.finish(record, code, signal);
|
|
246
247
|
});
|
|
248
|
+
// A login waits for a person at the browser window it just opened: say so on the desktop, where they are looking.
|
|
249
|
+
if (spec.kind === 'signin') {
|
|
250
|
+
this.notifyWaiting(record, 'AsRun — log in', `Log in to ${[spec.profile, spec.env].filter(Boolean).join(' @ ')}, then press "Done, save the session" on ` +
|
|
251
|
+
'the card in the browser window.');
|
|
252
|
+
}
|
|
247
253
|
this.events.emit('job', record.info);
|
|
248
254
|
return record.info;
|
|
249
255
|
}
|
|
@@ -476,9 +482,39 @@ class JobManager {
|
|
|
476
482
|
return;
|
|
477
483
|
if (now && before && before.since === new Date(now.since).toISOString())
|
|
478
484
|
return;
|
|
479
|
-
info.awaitingUser = now
|
|
485
|
+
info.awaitingUser = now
|
|
486
|
+
? { message: now.message, finish: now.finish, since: new Date(now.since).toISOString() }
|
|
487
|
+
: null;
|
|
480
488
|
this.persist(record);
|
|
481
489
|
this.events.emit('job', info);
|
|
490
|
+
// A handoff that starts right now: the person is told on the desktop, because the browser window is somewhere else
|
|
491
|
+
// and the page may be closed, focused or not allowed to notify. The dedupe keeps one handoff to one notification.
|
|
492
|
+
if (!before && now) {
|
|
493
|
+
this.notifyWaiting(record, 'AsRun — your turn', `${info.caseId ? `${info.caseId}: ` : ''}${now.message ?? 'A step in the browser window waits for you.'}` +
|
|
494
|
+
(now.finish ? ` Done when ${now.finish}.` : ''));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Shows a desktop notification now. Neither the agent (whose shell the CLI may refuse) nor the page (not open, not
|
|
499
|
+
* focused, not allowed) can be relied on for this, so the server shows it itself.
|
|
500
|
+
*/
|
|
501
|
+
notifyWaiting(record, title, body) {
|
|
502
|
+
notifyDesktop({ title, body, sound: true }, (reason) => this.pushNotificationFailure(record, reason));
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Why no notification appeared, written where the person reads next: never a reason to fail the job, and the next
|
|
506
|
+
* run of the same kind leaves no line at all when the notifier works.
|
|
507
|
+
*/
|
|
508
|
+
pushNotificationFailure(record, reason) {
|
|
509
|
+
const line = `\n[no desktop notification shown: ${reason}. "pnpm asrun notify-test" checks the notifier of this machine]\n`;
|
|
510
|
+
// The job may have ended while the notifier was still running: its log stream is closed then, so the files get the
|
|
511
|
+
// line directly (a running job is told through its stream, so the page shows it at once).
|
|
512
|
+
if (record.info.status === 'running' && record.logStream) {
|
|
513
|
+
this.pushPart(record, { kind: 'hint', at: Date.now(), data: line });
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
this.pushRecovered(record, line);
|
|
517
|
+
}
|
|
482
518
|
}
|
|
483
519
|
/**
|
|
484
520
|
* Stops a run whose agent has said nothing for `STALL_MS`: a tool call that never returns leaves the CLI waiting for
|
|
@@ -586,6 +622,10 @@ class JobManager {
|
|
|
586
622
|
: `[process exited with code ${code}]`);
|
|
587
623
|
if (record.spec.ai && info.status === 'succeeded')
|
|
588
624
|
info.resultState = parseResultState(resultText(record));
|
|
625
|
+
// The agent stopped with questions in the case file: that waits for a person exactly like a handoff card does.
|
|
626
|
+
if (info.resultState === 'needs-input') {
|
|
627
|
+
this.notifyWaiting(record, 'AsRun — answers needed', `${info.caseId ? `${info.caseId}: ` : 'The case: '}the agent wrote open questions into the case file.`);
|
|
628
|
+
}
|
|
589
629
|
this.push(record, 'system', `\n${summary}\n`);
|
|
590
630
|
record.logStream?.end();
|
|
591
631
|
record.chunkStream?.end();
|
|
@@ -134,8 +134,10 @@ const ATTENDED = 'Attended run: a person is at the machine and the browser windo
|
|
|
134
134
|
'need a choice to them and wait, as the skill describes. Start every handoff (also a second one in the same run) ' +
|
|
135
135
|
'with its own call that sets __handoffCfg, before the call that runs handoff-card.js, and never change those options ' +
|
|
136
136
|
'inside the call that runs the card: the web UI sees that a person is needed only from that call, so without it ' +
|
|
137
|
-
'there is no "Your turn" badge and no notification.
|
|
138
|
-
`
|
|
137
|
+
'there is no "Your turn" badge and no notification. Put one action per string in `steps` (the card numbers them), ' +
|
|
138
|
+
'keep `message` to one sentence, write the finish condition into `finish` as plain text, and set `doneText` to the ' +
|
|
139
|
+
'exact text that appears when the step is done so the card ends by itself. Nobody can answer ' +
|
|
140
|
+
`questions in the chat, so write open questions into the case file as the skill describes. ${RESULT_LINE_RULE}`;
|
|
139
141
|
/** The prompt of a one-shot run. Every command may leave open questions in the case, so every one ends with a RESULT line. */
|
|
140
142
|
export function headlessPrompt(command, caseId, options) {
|
|
141
143
|
const slash = slashCommand(command, caseId, options);
|
|
@@ -60,6 +60,8 @@ export interface JobInfo {
|
|
|
60
60
|
export interface AwaitingUserInfo {
|
|
61
61
|
/** What the person is asked to do, as the agent wrote it on the card; null when unknown. */
|
|
62
62
|
message: string | null;
|
|
63
|
+
/** The finish condition the card shows in its "DONE WHEN" block; null when the agent gave none. */
|
|
64
|
+
finish?: string | null;
|
|
63
65
|
since: string;
|
|
64
66
|
}
|
|
65
67
|
/** `needs-input`: the agent wrote open questions into the case file and cannot go on without answers. */
|
|
@@ -346,6 +348,12 @@ export interface ReportInfo {
|
|
|
346
348
|
counts: Record<ResultStatus, number> | null;
|
|
347
349
|
}
|
|
348
350
|
export type { ParsedError, ResultStatus, RunSettings, RunSummary, TestRunResult } from '../../lib/run-summary.js';
|
|
351
|
+
/**
|
|
352
|
+
* What an upgrade leaves to a person or an AI, and the prompt that hands it over. Built by `lib/upgrade-prompt.ts`,
|
|
353
|
+
* the same module `pnpm asrun upgrade --prompt --json` prints.
|
|
354
|
+
*/
|
|
355
|
+
export type { ScaffoldDrift, ScaffoldState } from '../../lib/scaffold.js';
|
|
356
|
+
export type { OutstandingFile, OutstandingMigration, UpgradeOutstanding, UpgradePromptPayload } from '../../lib/upgrade-prompt.js';
|
|
349
357
|
export interface InfoResponse {
|
|
350
358
|
root: string;
|
|
351
359
|
envs: string[];
|
|
@@ -94,7 +94,8 @@ Wrong: `- Create a draft named ... and delete it at the end.` (a write → a tag
|
|
|
94
94
|
- A question block is indented under the sentence it belongs to: 4 spaces under a numbered step, 2 under a bullet.
|
|
95
95
|
The parent line is the suggestion, written as a finished sentence; the `TODO` line holds one question and optional
|
|
96
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
|
|
97
|
+
Hang it under the immediate sentence, never under a broader sentence that merely contains it, and never inside the
|
|
98
|
+
`## Values` yaml.
|
|
98
99
|
Details: `.claude/skills/exploring-cases/references/open-questions.md`.
|
|
99
100
|
- A marker (`🔍 to be confirmed: <what a person must decide>`) is the alternative to a question, not a companion: it
|
|
100
101
|
says nobody has to decide, only to check. `(seen on <profile>)` is a plain note the run checks by itself. Its
|
|
@@ -148,6 +149,8 @@ One card per distinct thing a check looks at:
|
|
|
148
149
|
- `What`, `Where` and `Identified by` are required for a card that is not blocking.
|
|
149
150
|
- `Reference <profile-id>:` must point under `.explore/targets/<case-id>/` (git-ignored), or be `none: <reason>`.
|
|
150
151
|
A missing file on this machine is a warning, not a failure: a fresh checkout has the card but not the screenshot.
|
|
152
|
+
A card carries one line per profile that was looked at separately: an `--lead` exploration adds its own line for a
|
|
153
|
+
profile whose element differs, and `none: <reason>` for one that shares the lead's screenshot.
|
|
151
154
|
- A card may hold a `🚫 TODO [blocking]` block. Such a card is not complete; `pnpm case:check` does not fail on it,
|
|
152
155
|
and the check that uses it is not generated yet.
|
|
153
156
|
- Every `[T<n>]` in a step, a ready signal or a check resolves to a card. `pnpm case:check` fails on a reference with
|
|
@@ -207,6 +210,10 @@ test.describe('<Case title>', {tag: caseTags('<id>')}, () => {
|
|
|
207
210
|
by `pnpm asrun rules`) and the stamp is the run's UTC time; register it with `records.track` right after creating
|
|
208
211
|
it, remove it newest first (also after a failure), turn retries off with `configureCase`, and keep email steps
|
|
209
212
|
behind `emailStepAllowed(profile, '<step title>')`.
|
|
213
|
+
- `/explore-case --lead` explores one profile in full and the others only where they can differ. What a diff profile
|
|
214
|
+
adds to the case is its `## Values` entry and its `Reference <profile-id>:` line, never a second copy of the steps;
|
|
215
|
+
`/gen-spec` writes those profiles' parts from the lead's note and names them `not verified`. A step whose recipe
|
|
216
|
+
would differ per profile has no per-profile form (`- Recipe:` belongs to the step): split the case, or ask.
|
|
210
217
|
|
|
211
218
|
## Checks to run before reporting
|
|
212
219
|
|
|
@@ -124,16 +124,55 @@ A release can require an *existing* workspace to change; `docs/commands.md` has
|
|
|
124
124
|
- `version` is the release that introduces the change. `pnpm asrun migrate` runs an entry when
|
|
125
125
|
`migrations.since < version <= installed`, in version order, and moves `since` only after every entry of that
|
|
126
126
|
release has been processed, so a failed run repeats the release instead of skipping its other entries.
|
|
127
|
+
- **Never write a version by hand.** Release Please names the version in its release pull request, after the change is
|
|
128
|
+
merged, so an author cannot know it (and a guessed version that is too high never runs: `since` moves past it and the
|
|
129
|
+
workspace is silently skipped for a release). Write `id: 'unreleased-<slug>'` and `version: 'unreleased'`.
|
|
130
|
+
`resolveMigrations()` turns the sentinel into the installed release's own version — the release that carries the
|
|
131
|
+
code — so the runtime and the built manifest are always right; `pnpm migrations:freeze` rewrites the sentinel into a
|
|
132
|
+
real version and id, and `.github/workflows/release-freeze.yml` runs it on the release pull request so the id a
|
|
133
|
+
workspace sees in `doctor` names a release. That frozen version has no tag yet (the tag comes with the merge), so
|
|
134
|
+
`migrations:check` accepts the version `package.json` declares as the release being prepared — see below.
|
|
127
135
|
- `auto: true` with an `apply` runs after the install, when `detect` says the workspace is not migrated yet. Keep it
|
|
128
136
|
idempotent: a run that failed repeats the entry.
|
|
129
137
|
- Anything a person has to do is `manual: string[]`. The id is remembered in `.asrun.json` (`migrations.manual`) and
|
|
130
|
-
`doctor` keeps reminding until `pnpm asrun migrate --ack <id
|
|
131
|
-
- `
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
`doctor` keeps reminding until `pnpm asrun migrate --ack <id>`; every output prints that command with the real id.
|
|
139
|
+
- `files: string[]` names the project files the change touches. A once-only scaffold file there (`AGENTS.md`,
|
|
140
|
+
`.mcp.json`, ...) also has its template baseline advanced when the migration is acknowledged, so one `migrate --ack`
|
|
141
|
+
clears both the manual step and the file from the upgrade's `NEXT` block.
|
|
142
|
+
- `advisory: true` is for a release that touched a surface but asks nothing of an existing workspace (documentation, a
|
|
143
|
+
clarified contract, a new optional flag). It makes `pnpm migrations:check` pass without inventing manual steps, and
|
|
144
|
+
`migrate` reports it and moves on.
|
|
145
|
+
- `breaking: true` makes `pnpm asrun upgrade` ask before installing anything (`Proceed? [y/N]`; `--yes`/`-y` for an
|
|
146
|
+
unattended run). The pre-install check reads `dist/migrations/manifest.json` out of the target tarball, so
|
|
147
|
+
`pnpm build` has to write it (`scripts/build.ts` serialises `MIGRATIONS` with `toManifest`) and `verify:pack` asserts
|
|
148
|
+
the packed manifest carries real versions.
|
|
135
149
|
- Append only: never delete or renumber an entry, because a workspace on an older release may still need it. A change
|
|
136
150
|
that only concerns brand-new workspaces belongs in `templates/`, not in a migration.
|
|
151
|
+
|
|
152
|
+
**`pnpm migrations:check` is the gate.** It compares the working tree with the newest release tag and fails when a
|
|
153
|
+
change touched a surface that needs an entry (`templates/**` of a once-only file, the case document format, the rule
|
|
154
|
+
ids, the peer dependency range) and no entry names it, or when an entry is malformed (a duplicate id, an id that does
|
|
155
|
+
not start with the version it declares, neither `manual` nor `auto` nor `advisory`). It runs in `verify.yml`, so a
|
|
156
|
+
forgotten entry stops the pull request. `/check-migrations` in Claude Code walks the same steps and writes the entry.
|
|
157
|
+
|
|
158
|
+
An entry's version must be `unreleased` or a release that already has a tag. The single exception is the version
|
|
159
|
+
`package.json` declares: on the release pull request release-please has bumped it and `release-freeze.yml` has frozen
|
|
160
|
+
the entries to it, while the tag only appears when that pull request is merged. So `migrations:check` reads
|
|
161
|
+
`package.json` as the release being prepared — that version needs no tag and every entry has to stay at or below it,
|
|
162
|
+
which is exactly the "a guess that is too high never runs" rule. `doctor` warns about an entry above the installed
|
|
163
|
+
release for the same reason: `migrate` filters at the installed version and would silently skip it.
|
|
164
|
+
|
|
165
|
+
A command this repository uses itself lives in **both** `.claude/commands/` and `.opencode/commands/`: Claude Code reads
|
|
166
|
+
only the first, OpenCode only the second (it reloads them without a restart). The two files are identical and neither is
|
|
167
|
+
shipped for a workspace's own commands — those come from `templates/.claude/commands/` through `GENERATED`, which writes
|
|
168
|
+
each one into both folders of the workspace.
|
|
169
|
+
|
|
170
|
+
**The scaffold baseline** (`lib/scaffold.ts`, `.asrun.json` → `scaffold`) is what tells "the template changed and nobody
|
|
171
|
+
edited the file" (`replace`) from "both changed" (`merge`) and "no baseline yet" (`unknown-base`). `init` records it
|
|
172
|
+
when it writes a once-only file; `init --ack <file>` records it after a merge; nothing else writes it. A release that
|
|
173
|
+
changes a `templates/**` file a workspace owns therefore needs an entry with `files`, or the change reaches the user as
|
|
174
|
+
silent drift that only the baseline comparison reports.
|
|
175
|
+
|
|
137
176
|
- `scripts/migrate.ts` applies the *installed* package's registry, so it never runs an entry from a release that is not
|
|
138
177
|
installed. `scripts/upgrade.ts` installs first, then runs `init --update`, `migrate` and `doctor` through the newly
|
|
139
178
|
installed bin (`bin/asrun.mjs`), never through the process that started the upgrade.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
Four commands carry a test case from rough notes to a Playwright spec, and repair it when the product changes. They
|
|
4
4
|
work the same in Claude Code and OpenCode: each command is a thin shell that loads a skill from the framework package,
|
|
5
5
|
so the procedure lives in one place and a project can refine it (see
|
|
6
|
-
[Customizing the prompts](#customizing-the-prompts)).
|
|
6
|
+
[Customizing the prompts](#customizing-the-prompts)). A fifth, `/upgrade-workspace`, keeps the workspace itself in step
|
|
7
|
+
with the release it runs; it needs no environment and never touches what a test case means.
|
|
7
8
|
|
|
8
9
|
| Command | What it does |
|
|
9
10
|
|---|---|
|
|
10
11
|
| `/make-case <notes-or-path>` | Turn raw notes into case documents. Optional step. |
|
|
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
|
+
| `/explore-case <case-id> <env> [--attended] [--profiles a,b] [--lead <profile-id>] [--hints <file>]` | Open the real pages and write what they show into the case. |
|
|
12
13
|
| `/gen-spec <case-id> <env> [--force] [--hints <file>]` | Write `tests/<case-id>.spec.ts` and fill the case's `## Values`. |
|
|
13
14
|
| `/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. |
|
|
15
|
+
| `/upgrade-workspace` | Run `pnpm asrun upgrade --prompt` and follow it: merge the scaffold text of a project-owned file, fix case structure, run the checks. It never writes data, runs tests or acknowledges a migration step. |
|
|
14
16
|
|
|
15
|
-
The environment is **required** by every command except `/make-case`, which
|
|
17
|
+
The environment is **required** by every command except `/make-case` and `/upgrade-workspace`, which do not open
|
|
18
|
+
pages. There is no default.
|
|
16
19
|
|
|
17
20
|
`--profiles` exists on `/explore-case` and `/heal-spec`, not on `/gen-spec`: a spec runs once per profile in the
|
|
18
21
|
environment, so it is generated from every profile the case holds information for, never from a chosen subset.
|
|
@@ -88,10 +91,23 @@ publishes, shares or deletes anything, in staging or in production. It:
|
|
|
88
91
|
data-call paths it saw. `/gen-spec` reads it to verify instead of rediscovering, and a page that only exists after a
|
|
89
92
|
write step is written from it. Delete it at any time: without it, generation replays the case.
|
|
90
93
|
|
|
91
|
-
With many profiles, `--profiles a,b` opens only those (ids from `profiles.yaml`, comma-separated)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
With many profiles, `--profiles a,b` opens only those (ids from `profiles.yaml`, comma-separated), each in its own
|
|
95
|
+
session; without the flag the agent opens one logged-in profile per domain. Two different hosts are two domains even
|
|
96
|
+
when they serve the same product, so the list is never collapsed into one profile. The profiles the list leaves out are
|
|
97
|
+
not opened and nothing is written about them, so a case can be explored in passes: A and B first, C later. Runs add up;
|
|
98
|
+
the case keeps what the earlier runs recorded. In the web UI the profile picker of the toolbar does the same: every
|
|
99
|
+
profile that is logged in in that environment is ticked, and the list you tick is passed to the run and remembered for
|
|
100
|
+
the next one there.
|
|
101
|
+
|
|
102
|
+
`--lead <profile-id>` stops paying for the same flow on every profile. The lead profile is explored in full — every
|
|
103
|
+
step, every handoff, the recipe lines, an exploration note with every step — and each other selected profile only
|
|
104
|
+
where it can differ: its own `## Values` entries, its target screenshots and the steps that reach them. A case states
|
|
105
|
+
the same behaviour for every profile, so values and target screenshots are the only place a difference can live. A
|
|
106
|
+
profile whose page structure or flow turns out to differ from the lead's is explored in full instead, and the report
|
|
107
|
+
says so; a differing check that cannot be reached is a blocking question, never "same as the lead". A write step that a
|
|
108
|
+
differing check needs is still a handoff on that profile (test data is usually private to its account), so the person
|
|
109
|
+
still creates it — the saving is the pages and steps around it, not the write. Keep lead and diff profiles in one run:
|
|
110
|
+
the notes are written at its end and share one `caseSha256`, so a split run makes the earlier notes stale.
|
|
95
111
|
|
|
96
112
|
**To keep what the case says, delete the whole block.** **To answer, write after `Answer:`.** Then run the command
|
|
97
113
|
again. In the web UI the panel calls the first action **Keep what the case says**. The run's **What to do next** bar
|
|
@@ -104,9 +120,10 @@ restarted.
|
|
|
104
120
|
Use it for a case that creates or edits data, or needs a choice (an existing document, items). Stay at the computer:
|
|
105
121
|
the browser window opens and the agent waits while you do those steps.
|
|
106
122
|
|
|
107
|
-
- The window shows a small draggable card in the bottom-left corner
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
- The window shows a small draggable card in the bottom-left corner: one sentence about the step, the steps numbered,
|
|
124
|
+
the texts to copy, and a green **DONE WHEN** block with the finish condition. **Done** hands control back, and
|
|
125
|
+
**Cannot do this** gives the step up. The agent also continues by itself when the finish condition shows up (for
|
|
126
|
+
example a "Saved" message); the card turns green when control is back. It waits up to 10 minutes per step.
|
|
110
127
|
- Recording starts and stops by itself. If Playwright's own recording toolbar shows at the top of the page, leave it
|
|
111
128
|
alone: it can stop the recording.
|
|
112
129
|
- The card records what you did, and the agent writes it into the case under the step as **general recipe lines**
|
package/docs/cases.md
CHANGED
|
@@ -113,11 +113,11 @@ report and the trace read like the case.
|
|
|
113
113
|
exploration: positions instead of names of changeable data (`select items #1 to #3`), created names (project prefix
|
|
114
114
|
plus run stamp), control labels but no selectors. Several lines per step are normal. A tagged step with no recipe is
|
|
115
115
|
not generated: `/gen-spec` stops with `needs-input`.
|
|
116
|
-
- **Open questions** go in a `🚫 TODO [blocking]` or `❓ TODO [non-blocking]` block under the sentence they are about
|
|
117
|
-
|
|
118
|
-
whole block; answering means writing after `Answer:`. In the web panel the two actions are **Keep what
|
|
119
|
-
says** and **Write your answer**. Hang the block under the sentence that makes the claim, not under a
|
|
120
|
-
|
|
116
|
+
- **Open questions** go in a `🚫 TODO [blocking]` or `❓ TODO [non-blocking]` block under the sentence they are about:
|
|
117
|
+
that sentence is the agent's suggestion, and the block's last line is `- ✍️ Answer:`. Accepting a question means
|
|
118
|
+
deleting the whole block; answering means writing after `Answer:`. In the web panel the two actions are **Keep what
|
|
119
|
+
the case says** and **Write your answer**. Hang the block under the sentence that makes the claim, not under a
|
|
120
|
+
broader sentence that merely contains it, and one claim gets one block.
|
|
121
121
|
- **What the AI saw** is marked by what happens if it is wrong: `(seen on <profile>)` is a plain note the run checks by
|
|
122
122
|
itself, and `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)` is a line that would pass even
|
|
123
123
|
if it were wrong, so a person settles it. A marker may name a screenshot (`evidence: .explore/.../x.png`) so nobody
|
package/docs/commands.md
CHANGED
|
@@ -8,7 +8,8 @@ thin wrappers, so `pnpm asrun`, `pnpm signin` and the rest work as below. The sa
|
|
|
8
8
|
| I want to... | Command |
|
|
9
9
|
|---|---|
|
|
10
10
|
| Install the framework in a project | `pnpm dlx @asrunjs/asrun init --install` |
|
|
11
|
-
| Upgrade the framework and migrate the workspace | `pnpm asrun upgrade [--version <v>] [--dry-run]` |
|
|
11
|
+
| Upgrade the framework and migrate the workspace | `pnpm asrun upgrade [--version <v>] [--dry-run] [--yes]` |
|
|
12
|
+
| Let an AI finish what an upgrade left to the project | `pnpm asrun upgrade --prompt`, or `/upgrade-workspace` |
|
|
12
13
|
| Prepare or check the workspace | `pnpm asrun init [--update]`, `pnpm asrun doctor` |
|
|
13
14
|
| See the effective rules and project settings | `pnpm asrun rules [--json] [--case <case-id>]` |
|
|
14
15
|
| See or apply the workspace migrations after a manual install | `pnpm asrun migrate [--list] [--dry-run]` |
|
|
@@ -16,6 +17,7 @@ thin wrappers, so `pnpm asrun`, `pnpm signin` and the rest work as below. The sa
|
|
|
16
17
|
| Start the web UI | `pnpm go`, `pnpm ui`, `pnpm ui:dev` |
|
|
17
18
|
| See which profiles are logged in | `pnpm status [--env <env>]` |
|
|
18
19
|
| Log in a profile (a person, with 2FA) | `pnpm signin <profile-id>` |
|
|
20
|
+
| Check that desktop notifications reach you | `pnpm asrun notify-test` |
|
|
19
21
|
| Run tests | `pnpm asrun --env <env> [filters]` |
|
|
20
22
|
| Open the last report | `pnpm report` |
|
|
21
23
|
| Look around a page as a profile | `pnpm explore <profile-id> [path]` |
|
|
@@ -116,6 +118,23 @@ pnpm signin example-staging
|
|
|
116
118
|
The login is done by a person, including 2FA; the session is saved to `.auth/<profile-id>.json` and verified. See
|
|
117
119
|
[profiles.md](./profiles.md).
|
|
118
120
|
|
|
121
|
+
## Notifications
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pnpm asrun notify-test
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The UI server shows a **desktop notification** when a run waits for you: an AI run handed a step to the browser window,
|
|
128
|
+
a login opened its window, or a run ended with open questions in the case file. It is the server's own call
|
|
129
|
+
(`osascript` on macOS, `notify-send` on Linux, a PowerShell toast on Windows), so it arrives whether or not the UI page
|
|
130
|
+
is open, focused or allowed to notify. The browser's bell is separate: it is for "a run ended", while the tab is in the
|
|
131
|
+
background (see [web-ui.md](./web-ui.md)).
|
|
132
|
+
|
|
133
|
+
`notify-test` shows one through the same code and prints the command it ran. Exit code 0 with nothing on screen means
|
|
134
|
+
the operating system dropped the notification, not that asrun failed: allow the app that runs the command (Terminal,
|
|
135
|
+
iTerm2, ...) in its notifications settings. The job log says `[no desktop notification shown: ...]` when the command
|
|
136
|
+
itself failed.
|
|
137
|
+
|
|
119
138
|
## Cases and checks
|
|
120
139
|
|
|
121
140
|
```bash
|
|
@@ -155,10 +174,12 @@ step, error, call log and screenshots for a case, and whether the report is stal
|
|
|
155
174
|
first.
|
|
156
175
|
|
|
157
176
|
`explore:notes` reads the **exploration note** that `/explore-case` writes for a case under
|
|
158
|
-
`.explore/notes/<case-id>/<profile-id>.json`. It prints one line per profile (fresh or stale,
|
|
159
|
-
covers) and `--json` prints the parsed notes for a run that reads
|
|
177
|
+
`.explore/notes/<case-id>/<profile-id>.json`. It prints one line per profile (fresh or stale, `full` or `diff` with the
|
|
178
|
+
lead profile it leans on, and the steps and targets it covers) and `--json` prints the parsed notes for a run that reads
|
|
179
|
+
them. A note is local, git-ignored and optional:
|
|
160
180
|
`/gen-spec` uses a fresh one to verify the locators and request patterns instead of rediscovering them, and replays the
|
|
161
|
-
case when it is missing or stale. `--check` fails on a malformed note and warns about a stale one
|
|
181
|
+
case when it is missing or stale. `--check` fails on a malformed note and warns about a stale one or a `diff` note whose
|
|
182
|
+
lead has no note (it prints the current
|
|
162
183
|
`caseSha256`); `--hash` prints that hash. Deleting `.explore/notes/` is always safe.
|
|
163
184
|
|
|
164
185
|
## Rules
|
|
@@ -224,15 +245,47 @@ pnpm asrun upgrade --version 0.9.0
|
|
|
224
245
|
`installed -> target`;
|
|
225
246
|
2. reads the **migration manifest of the target release out of its tarball, before installing anything**, and prints
|
|
226
247
|
the migrations and breaking changes between the workspace's baseline (`.asrun.json` → `migrations.since`) and that
|
|
227
|
-
release. A breaking change needs a person's confirmation (
|
|
228
|
-
release that ships no manifest (it predates the manifest) cannot be checked in
|
|
229
|
-
before it installs;
|
|
248
|
+
release. A breaking change needs a person's confirmation (`Proceed? [y/N]`, an empty line means no); an unattended
|
|
249
|
+
run passes `--yes` (or `-y`). A release that ships no manifest (it predates the manifest) cannot be checked in
|
|
250
|
+
advance: `upgrade` says so and asks before it installs;
|
|
230
251
|
3. installs `@asrunjs/asrun@<target>` (it first exempts a fresh release from pnpm's release age, or pnpm would install
|
|
231
252
|
the previous one);
|
|
232
253
|
4. runs `init --update`, `migrate` and `doctor` through the package that was just installed.
|
|
233
254
|
|
|
234
255
|
`--dry-run` stops after step 2. `--version` pins a release; `upgrade` never downgrades.
|
|
235
256
|
|
|
257
|
+
### What an upgrade leaves to a person or an AI
|
|
258
|
+
|
|
259
|
+
`init --update` refreshes only the files the framework generates. A release can also change a file the *project* owns
|
|
260
|
+
(`AGENTS.md`, `CLAUDE.md`, `.mcp.json`, `opencode.jsonc`, `tsconfig.json`, `pnpm-workspace.yaml`, `profiles.yaml`,
|
|
261
|
+
`.claude/settings.json`, `cases/_template.md`) or ask for a manual migration step. Both are recorded, and `upgrade` ends
|
|
262
|
+
with a framed `NEXT` block that lists them with the exact command for each. When there is nothing to do it prints one
|
|
263
|
+
quiet line instead.
|
|
264
|
+
|
|
265
|
+
`pnpm asrun upgrade --prompt` prints that work as a task for an AI agent, and changes nothing (it reads the workspace
|
|
266
|
+
and the installed package only, so it works offline):
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
pnpm asrun upgrade --prompt # the task, as Markdown
|
|
270
|
+
pnpm asrun upgrade --prompt --json # the same answer as JSON (the web UI uses this)
|
|
271
|
+
pnpm asrun migrate --prompt # the same, after a manual "pnpm add" + "init --update"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
In Claude Code or OpenCode, `/upgrade-workspace` does the same: it runs the command above and follows its output.
|
|
275
|
+
|
|
276
|
+
The prompt is the same for every release; the release-specific part is data — which migrations are outstanding, and
|
|
277
|
+
which project files the templates moved under. For each project file it says how to resolve it:
|
|
278
|
+
|
|
279
|
+
| File state | Meaning | Command |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `replace` | the template changed and nobody edited the file | `pnpm asrun init --force <file>` |
|
|
282
|
+
| `merge` | the template and the project both changed it | merge the template's part, then `pnpm asrun init --ack <file>` |
|
|
283
|
+
| `unknown-base` | the workspace has no recorded template for the file | compare and report, then `pnpm asrun init --ack <file>` |
|
|
284
|
+
| `missing` | the template has the file and the workspace does not | `pnpm asrun init` |
|
|
285
|
+
|
|
286
|
+
`init --ack <file>` only records that the file is aligned with the installed templates; it never edits the file. The
|
|
287
|
+
baseline lives in `.asrun.json` (`scaffold`) and is committed, so the whole team sees the same state after a `git pull`.
|
|
288
|
+
|
|
236
289
|
Every `asrun` command also checks the registry for a newer release, at most once a day (the answer is cached in `.tmp/`
|
|
237
290
|
and shared with the web UI), and prints one line on stderr before it starts:
|
|
238
291
|
`@asrunjs/asrun 0.8.0 is available (installed 0.7.0). Upgrade: pnpm asrun upgrade`. On a terminal the line carries an
|
|
@@ -257,11 +310,14 @@ can simply be repeated. A migration marked `auto` runs by itself; one with `manu
|
|
|
257
310
|
until a person does it and acknowledges it. `init --update` never advances the baseline, which is why a manual
|
|
258
311
|
`pnpm add -D @asrunjs/asrun@latest` followed by `pnpm asrun init --update` still leaves its migrations pending.
|
|
259
312
|
|
|
313
|
+
A migration that names project files (`files: ['AGENTS.md']`) also advances those files' template baseline when it is
|
|
314
|
+
acknowledged, so one command clears both the manual step and the `NEXT` block reminder about the file.
|
|
315
|
+
|
|
260
316
|
```bash
|
|
261
317
|
pnpm asrun migrate # apply what the installed release brings
|
|
262
318
|
pnpm asrun migrate --dry-run # show it, change nothing
|
|
263
319
|
pnpm asrun migrate --list # every migration of the installed release, and its state
|
|
264
|
-
pnpm asrun migrate --ack <id> # a manual step is done (--ack-all for every one)
|
|
320
|
+
pnpm asrun migrate --ack <id> # a manual step is done (--ack-all for every one); no file is touched
|
|
265
321
|
pnpm asrun migrate --to <v> # stop at that release
|
|
266
322
|
```
|
|
267
323
|
|
|
@@ -271,20 +327,22 @@ migration is the installed package. `doctor` warns while migrations or manual st
|
|
|
271
327
|
## Workspace
|
|
272
328
|
|
|
273
329
|
```bash
|
|
274
|
-
pnpm asrun init [--update] [--force] [--force <file>] [--reset-rules] [--root <dir>] [--no-env] [--no-extras] [--install [spec]]
|
|
330
|
+
pnpm asrun init [--update] [--force] [--force <file>] [--reset-rules] [--ack <file>] [--root <dir>] [--no-env] [--no-extras] [--install [spec]]
|
|
275
331
|
pnpm asrun doctor
|
|
276
|
-
pnpm asrun upgrade [--version <v>] [--dry-run] [--yes]
|
|
277
|
-
pnpm asrun migrate [--list] [--dry-run] [--yes] [--ack <id>|--ack-all]
|
|
332
|
+
pnpm asrun upgrade [--version <v>] [--dry-run] [--yes|-y] [--prompt [--json]]
|
|
333
|
+
pnpm asrun migrate [--list] [--dry-run] [--yes|-y] [--ack <id>|--ack-all] [--prompt [--json]]
|
|
278
334
|
pnpm asrun reset [--root <dir>] [--dry-run] [--yes] [--state] [--lockfile]
|
|
279
335
|
```
|
|
280
336
|
|
|
281
337
|
`init` writes the workspace files; `--update` refreshes only the generated ones, and `--force` rewrites every file
|
|
282
338
|
init writes (a single file with `--force <file>`). `--force` still keeps `profiles.yaml` and `package.json`; name
|
|
283
|
-
`--force profiles.yaml` to replace profiles, and add `--reset-rules` to replace `AGENTS.md` or `CLAUDE.md`.
|
|
284
|
-
|
|
285
|
-
|
|
339
|
+
`--force profiles.yaml` to replace profiles, and add `--reset-rules` to replace `AGENTS.md` or `CLAUDE.md`. `--ack
|
|
340
|
+
<file>` records that a project-owned file is aligned with the installed templates without editing it (after a merge).
|
|
341
|
+
It also adds the `allowBuilds` entries `pnpm-workspace.yaml` lacks, and installs the optional packages this machine
|
|
342
|
+
lacks (`ffmpeg-static`; `--no-extras` skips them).
|
|
286
343
|
`doctor` checks profiles, `tsconfig.json`, `playwright.config.ts`, the generated files, the installed version, the
|
|
287
|
-
project rule table, the skill overrides
|
|
344
|
+
project rule table, the skill overrides, the workspace migrations and the PDF renderer (a missing or unloadable
|
|
345
|
+
`pdfjs-dist` fails every PDF check). `upgrade` and `migrate` are described above.
|
|
288
346
|
|
|
289
347
|
`reset` removes `node_modules` and the files `init` writes, for a clean reinstall. It deletes a once-only file only
|
|
290
348
|
when it is still identical to its template, so edited files stay; `cases/`, specs, baselines, `profiles.yaml`,
|
package/docs/install.md
CHANGED
|
@@ -6,7 +6,7 @@ and is never edited: a project commits only its own test content.
|
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
|
-
- **Node.js 22 or newer** and **pnpm**.
|
|
9
|
+
- **Node.js 22.13 or newer** and **pnpm**.
|
|
10
10
|
- **ffmpeg** joins the recordings of one test (a new browser tab has its own video) into one video for the report.
|
|
11
11
|
`pnpm go` installs it with Homebrew on macOS or winget on Windows; on Linux run `sudo apt install ffmpeg`. When it is
|
|
12
12
|
missing, `pnpm go` / `pnpm asrun init` install the bundled `ffmpeg-static` package into the workspace; if neither
|
|
@@ -123,10 +123,22 @@ pnpm asrun upgrade
|
|
|
123
123
|
One command installs the newest release, refreshes the generated files to its templates, applies the workspace
|
|
124
124
|
migrations and runs `doctor`. Before it installs anything it reads the migration manifest of the target release and
|
|
125
125
|
prints the breaking changes and migrations between the workspace's baseline and that release; a breaking change needs
|
|
126
|
-
a person's confirmation (
|
|
127
|
-
changes nothing; `--version <v>` pins a release; it never downgrades.
|
|
126
|
+
a person's confirmation (`Proceed? [y/N]`, an empty line cancels), and an unattended run passes `--yes` (or `-y`).
|
|
127
|
+
`--dry-run` shows the same plan and changes nothing; `--version <v>` pins a release; it never downgrades.
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
The upgrade ends with a `NEXT` block when the release changed something the project owns (`AGENTS.md`, `.mcp.json`,
|
|
130
|
+
`cases/_template.md`, ...) or recorded manual migration steps. Each line carries the exact command; an AI can do the
|
|
131
|
+
whole block:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pnpm asrun upgrade --prompt
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
That prints the task as Markdown and changes nothing. Paste it into any agent, or — in Claude Code or OpenCode — run
|
|
138
|
+
`/upgrade-workspace`, which does the same. The prompt is the same for every release: the release-specific part (which
|
|
139
|
+
files moved, which steps are open) is read from the workspace and the installed package.
|
|
140
|
+
|
|
141
|
+
`upgrade` arrives with this release, so a workspace that predates `upgrade` updates once by hand:
|
|
130
142
|
|
|
131
143
|
```bash
|
|
132
144
|
pnpm add -D @asrunjs/asrun@latest
|
|
@@ -142,7 +154,8 @@ you when the workspace is behind the installed version, and what is still to mig
|
|
|
142
154
|
|
|
143
155
|
An upgrade never replaces what belongs to the project: `cases/`, `tests/` with their baselines, `profiles.yaml`,
|
|
144
156
|
`AGENTS.md`, `CLAUDE.md`, the skill overrides and `.claude/settings.json` stay as they are. A release that needs a
|
|
145
|
-
project-owned file to change says so as a migration with manual steps
|
|
157
|
+
project-owned file to change says so as a migration with manual steps, and the `NEXT` block (or
|
|
158
|
+
`pnpm asrun upgrade --prompt`) tells a person or an agent what to merge.
|
|
146
159
|
|
|
147
160
|
To go back to an older release, install it yourself and refresh:
|
|
148
161
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -45,6 +45,23 @@ bundled `ffmpeg-static` package into the workspace; if neither works, the report
|
|
|
45
45
|
|
|
46
46
|
**The UI port is busy** — set `ASRUN_UI_PORT` (and `ASRUN_UI_WEB_PORT` for `pnpm ui:dev`).
|
|
47
47
|
|
|
48
|
+
## Notifications
|
|
49
|
+
|
|
50
|
+
**"It is my turn" showed no notification** — run `pnpm asrun notify-test`. It shows one notification through the same
|
|
51
|
+
code the UI server uses and prints the command behind it, so the two failures are told apart:
|
|
52
|
+
|
|
53
|
+
- It prints `No notification: ...` — the command itself failed (not installed, refused, timed out). The job log has the
|
|
54
|
+
same reason as `[no desktop notification shown: ...]`.
|
|
55
|
+
- It prints `Done` and nothing appeared — the operating system dropped it. Allow the app that runs it (Terminal, iTerm2,
|
|
56
|
+
the editor's terminal on macOS; Windows PowerShell on Windows) in the notifications settings of the system, and turn
|
|
57
|
+
off Focus assist / Do Not Disturb. asrun cannot see or fix this: the notification reached the system.
|
|
58
|
+
- Linux without `libnotify` has no `notify-send`: install it (`sudo apt install libnotify-bin`).
|
|
59
|
+
|
|
60
|
+
**A run that ended showed no browser notification** — that is the bell in the top bar, and it only fires while its tab is
|
|
61
|
+
in the background: the switch must be on, the browser allowed to notify for this site, and the browser itself allowed in
|
|
62
|
+
the operating system. Its tooltip says when the browser or the system refuses it. Nothing that waits for you depends on
|
|
63
|
+
it: a login, a handoff card and open questions are desktop notifications from the server.
|
|
64
|
+
|
|
48
65
|
## Cases and specs
|
|
49
66
|
|
|
50
67
|
**`Missing values: cases/<id>.md has no "## Values" for profile ...`** — add the profile's values (or a `default`
|