@asrunjs/asrun 0.2.0 → 0.5.3
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 +18 -15
- package/.claude/commands/gen-spec.md +16 -13
- package/.claude/commands/heal-spec.md +26 -23
- package/.claude/commands/make-case.md +11 -8
- package/.claude/skills/exploring-cases/SKILL.md +146 -132
- package/.claude/skills/exploring-cases/references/open-questions.md +116 -112
- package/.claude/skills/exploring-cases/references/readiness.md +146 -137
- package/.claude/skills/exploring-cases/references/user-handoff.md +157 -147
- package/.claude/skills/generating-specs/SKILL.md +219 -207
- package/.claude/skills/generating-specs/references/conventions.md +148 -148
- package/.claude/skills/making-cases/SKILL.md +72 -64
- package/.claude/skills/making-cases/references/normalizing.md +39 -38
- package/.claude/skills/making-cases/references/writes-and-email.md +37 -36
- package/AGENTS.md +169 -154
- package/CLAUDE.md +2 -1
- package/README.md +221 -410
- package/dist/lib/build-artifacts.d.ts +8 -0
- package/dist/lib/build-artifacts.js +10 -0
- package/dist/lib/case-doc.d.ts +311 -0
- package/dist/lib/case-doc.js +1117 -0
- package/dist/lib/cases.d.ts +2 -15
- package/dist/lib/cases.js +5 -78
- package/dist/lib/command.d.ts +24 -0
- package/dist/lib/command.js +90 -0
- package/dist/lib/fixtures.js +2 -2
- package/dist/lib/login-card.d.ts +22 -0
- package/dist/lib/login-card.js +183 -0
- package/dist/lib/node-cli.d.ts +6 -2
- package/dist/lib/node-cli.js +16 -6
- package/dist/lib/profiles.d.ts +25 -1
- package/dist/lib/profiles.js +54 -1
- package/dist/lib/run-filters.js +10 -2
- package/dist/lib/session.d.ts +13 -1
- package/dist/lib/session.js +73 -7
- package/dist/lib/targets.d.ts +5 -13
- package/dist/lib/targets.js +4 -29
- package/dist/lib/values.js +2 -2
- package/dist/scripts/build.js +2 -1
- package/dist/scripts/check-cases.js +74 -6
- package/dist/scripts/doctor.js +115 -4
- package/dist/scripts/explore.js +22 -1
- package/dist/scripts/go.mjs +188 -188
- package/dist/scripts/init.d.ts +7 -1
- package/dist/scripts/init.js +62 -14
- package/dist/scripts/lint-specs.js +4 -1
- package/dist/scripts/login.js +30 -2
- package/dist/ui/server/actions.d.ts +4 -0
- package/dist/ui/server/actions.js +25 -12
- package/dist/ui/server/ai-models.d.ts +3 -0
- package/dist/ui/server/ai-models.js +347 -0
- package/dist/ui/server/ai-stream.d.ts +34 -9
- package/dist/ui/server/ai-stream.js +283 -77
- package/dist/ui/server/app.js +74 -12
- package/dist/ui/server/jobs.d.ts +7 -1
- package/dist/ui/server/jobs.js +69 -15
- package/dist/ui/server/profiles.d.ts +20 -1
- package/dist/ui/server/profiles.js +214 -38
- package/dist/ui/shared/ai-commands.d.ts +16 -6
- package/dist/ui/shared/ai-commands.js +35 -12
- package/dist/ui/shared/types.d.ts +114 -0
- package/docs/agents/case-format.md +193 -0
- package/docs/agents/framework-dev.md +167 -0
- package/docs/ai-workflow.md +235 -0
- package/docs/cases.md +189 -0
- package/docs/commands.md +175 -0
- package/docs/install.md +174 -0
- package/docs/profiles.md +139 -0
- package/docs/theme-guide.html +922 -692
- package/docs/troubleshooting.md +101 -0
- package/docs/web-ui.md +139 -0
- package/package.json +20 -1
- package/templates/.claude/commands/explore-case.md +22 -21
- package/templates/.claude/commands/gen-spec.md +21 -20
- package/templates/.claude/commands/heal-spec.md +31 -30
- package/templates/.claude/commands/make-case.md +13 -12
- package/templates/.mcp.json +8 -8
- package/templates/AGENTS.md +66 -45
- package/templates/cases/_template.md +57 -63
- package/templates/opencode.jsonc +16 -11
- package/templates/profiles.yaml +42 -29
- package/ui/dist/assets/{dist-D_I45xgJ.js → dist-BudNAjOv.js} +1 -1
- package/ui/dist/assets/{dist-CBV7_vDN.js → dist-C-eCSFja.js} +1 -1
- package/ui/dist/assets/{dist-gF2bFn-5.js → dist-CO4heSzh.js} +1 -1
- package/ui/dist/assets/{dist-DX8knLVr.js → dist-CPLhRzom.js} +1 -1
- package/ui/dist/assets/{dist-BqAROxzI.js → dist-DEPJZ3Me.js} +1 -1
- package/ui/dist/assets/{dist-BwYRbe5s.js → dist-DZ7RiqmA.js} +1 -1
- package/ui/dist/assets/{dist-BHaIOZdW.js → dist-LixzGniF.js} +1 -1
- package/ui/dist/assets/{dist-DHCY9KpV.js → dist-O_RVi4tP.js} +1 -1
- package/ui/dist/assets/{dist-BiE4suo9.js → dist-jPS6HCJ5.js} +1 -1
- package/ui/dist/assets/{dist-BSJMM_jp.js → dist-vmf3fnkM.js} +1 -1
- package/ui/dist/assets/index-BIGt8Hfz.js +338 -0
- package/ui/dist/assets/index-CTmnF0Wp.css +2 -0
- package/ui/dist/help/ai-tools.png +0 -0
- package/ui/dist/help/case-explore.png +0 -0
- package/ui/dist/help/case-generate.png +0 -0
- package/ui/dist/help/cases-memo.png +0 -0
- package/ui/dist/help/profile-form.png +0 -0
- package/ui/dist/help/profiles-login.png +0 -0
- package/ui/dist/help/reports.png +0 -0
- package/ui/dist/help/run-tests.png +0 -0
- package/ui/dist/index.html +22 -16
- package/ui/dist/assets/index-DmasQ6Rq.css +0 -2
- package/ui/dist/assets/index-I7uU93kk.js +0 -294
|
@@ -68,10 +68,25 @@ const STREAM_ARGS = {
|
|
|
68
68
|
claude: ['--output-format', 'stream-json', '--verbose'],
|
|
69
69
|
opencode: ['--format', 'json'],
|
|
70
70
|
};
|
|
71
|
-
function invocation(tool, prompt, writes, session) {
|
|
71
|
+
function invocation(tool, prompt, writes, session, model, thinking) {
|
|
72
72
|
// OpenCode creates the session when the id does not exist yet, and continues it when it does.
|
|
73
|
+
// A model is named on every run: without one OpenCode falls back to its built-in free model, which has a daily
|
|
74
|
+
// usage limit, and nothing in the CLI's own state (the TUI's last-used model) applies to a one-shot run.
|
|
73
75
|
if (tool === 'opencode')
|
|
74
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
file: 'opencode',
|
|
78
|
+
args: [
|
|
79
|
+
'run',
|
|
80
|
+
'--auto',
|
|
81
|
+
...STREAM_ARGS.opencode,
|
|
82
|
+
...(session ? ['-s', session.id] : []),
|
|
83
|
+
...(model ? ['--model', model] : []),
|
|
84
|
+
// Without it OpenCode does not ask the model to think, so the reasoning it does would never reach the log.
|
|
85
|
+
// Claude Code has no such flag: it shows thinking when the model and its settings have it on.
|
|
86
|
+
...(thinking ? ['--thinking'] : []),
|
|
87
|
+
prompt,
|
|
88
|
+
],
|
|
89
|
+
};
|
|
75
90
|
return {
|
|
76
91
|
file: 'claude',
|
|
77
92
|
args: [
|
|
@@ -81,6 +96,7 @@ function invocation(tool, prompt, writes, session) {
|
|
|
81
96
|
...(session ? [session.resume ? '--resume' : '--session-id', session.id] : []),
|
|
82
97
|
// `acceptEdits` approves edits anywhere in the folder, which would void the narrow Edit rules below.
|
|
83
98
|
...(writes === 'files' || writes === 'spec' ? ['--permission-mode', 'acceptEdits'] : []),
|
|
99
|
+
...(model ? ['--model', model] : []),
|
|
84
100
|
'--allowedTools',
|
|
85
101
|
...claudeAllowedTools(writes),
|
|
86
102
|
'--disallowedTools',
|
|
@@ -100,7 +116,10 @@ const NON_INTERACTIVE = 'Non-interactive run: nobody can answer questions, so do
|
|
|
100
116
|
* browser window, so the agent hands them the steps that write data and waits, as user-handoff.md describes.
|
|
101
117
|
*/
|
|
102
118
|
const ATTENDED = 'Attended run: a person is at the machine and the browser window is visible, so hand steps that write data or ' +
|
|
103
|
-
'need a choice to them and wait, as the skill describes.
|
|
119
|
+
'need a choice to them and wait, as the skill describes. Start every handoff (also a second one in the same run) ' +
|
|
120
|
+
'with its own call that sets __handoffCfg, before the call that runs handoff-card.js, and never change those options ' +
|
|
121
|
+
'inside the call that runs the card: the web UI sees that a person is needed only from that call, so without it ' +
|
|
122
|
+
'there is no "Your turn" badge and no notification. Nobody can answer questions in the chat, so write open ' +
|
|
104
123
|
'questions into the case file as the skill describes and end with the RESULT: line.';
|
|
105
124
|
/** The prompt of a one-shot run. `heal-spec` has no questions of its own, so it keeps the plain slash command. */
|
|
106
125
|
export function headlessPrompt(command, caseId, options) {
|
|
@@ -113,7 +132,7 @@ export function headlessPrompt(command, caseId, options) {
|
|
|
113
132
|
}
|
|
114
133
|
/** Executable and arguments of the one-shot (non-interactive) run. */
|
|
115
134
|
export function headlessInvocation(tool, command, caseId, options, session) {
|
|
116
|
-
return invocation(tool, headlessPrompt(command, caseId, options), AI_COMMAND_WRITES[command], session);
|
|
135
|
+
return invocation(tool, headlessPrompt(command, caseId, options), AI_COMMAND_WRITES[command], session, options.model, options.thinking);
|
|
117
136
|
}
|
|
118
137
|
/** The one-shot command as it is typed in a shell. */
|
|
119
138
|
export function headlessCommandLine(tool, command, caseId, options, session) {
|
|
@@ -122,23 +141,27 @@ export function headlessCommandLine(tool, command, caseId, options, session) {
|
|
|
122
141
|
/** Folder (relative to the AsRun root, git-ignored) where the clues typed into the Heal dialog are stored. */
|
|
123
142
|
export const HINTS_DIR = '.tmp/hints';
|
|
124
143
|
export const HINTS_MAX_CHARS = 8000;
|
|
144
|
+
/** Folder (relative to the AsRun root, git-ignored) where the answers typed into the Continue box are stored. */
|
|
145
|
+
export const REPLY_DIR = '.tmp/replies';
|
|
125
146
|
export const REPLY_MAX_CHARS = 4000;
|
|
126
147
|
export const USE_DEFAULTS_REPLY = 'Accept the suggestion of every open question: delete its TODO block and keep the sentence above it as confirmed text.';
|
|
127
148
|
/**
|
|
128
149
|
* The next turn of a run that ended with `RESULT: needs-input`: the user's answers. They go into the case file, which
|
|
129
|
-
* stays the record of truth, and the task of the original command is finished.
|
|
150
|
+
* stays the record of truth, and the task of the original command is finished. The answers come from a file
|
|
151
|
+
* (`.tmp/replies/…md`) rather than from the prompt, so a line break or a quote in them cannot break the command line.
|
|
130
152
|
*/
|
|
131
|
-
export function replyPrompt(command,
|
|
132
|
-
return (`The user answers your open questions (still nobody to ask, so do not stop to ask again)
|
|
153
|
+
export function replyPrompt(command, replyFile) {
|
|
154
|
+
return (`The user answers your open questions (still nobody to ask, so do not stop to ask again). ` +
|
|
155
|
+
`Their answers are in ${replyFile}, relative to the project root: read that file first.\n` +
|
|
133
156
|
'Fold every answer into the case file as the skill describes: write the answer into the sentence above the ' +
|
|
134
157
|
'TODO block and remove the block, and keep a TODO block (with an empty Answer: line, emoji prefixes included) for what is still unknown. ' +
|
|
135
158
|
`Then finish the task of /${command} and end with the RESULT: line.`);
|
|
136
159
|
}
|
|
137
160
|
/** The next turn of an earlier run, in its session and with exactly the permissions of the first turn. */
|
|
138
|
-
export function continueInvocation(tool, command,
|
|
139
|
-
return invocation(tool, replyPrompt(command,
|
|
161
|
+
export function continueInvocation(tool, command, replyFile, sessionId, model, thinking) {
|
|
162
|
+
return invocation(tool, replyPrompt(command, replyFile), AI_COMMAND_WRITES[command], { id: sessionId, resume: true }, model, thinking);
|
|
140
163
|
}
|
|
141
|
-
export const continueCommandLine = (tool, command,
|
|
164
|
+
export const continueCommandLine = (tool, command, replyFile, sessionId, model, thinking) => commandLine(continueInvocation(tool, command, replyFile, sessionId, model, thinking));
|
|
142
165
|
/** Folder (relative to the AsRun root, git-ignored) where memos from the Cases page are stored. */
|
|
143
166
|
export const MEMO_DIR = '.tmp/memos';
|
|
144
167
|
export const MEMO_MAX_CHARS = 100_000;
|
|
@@ -150,5 +173,5 @@ export const makeCasePrompt = (memoPath) => `/make-case ${memoPath} ` +
|
|
|
150
173
|
'Never overwrite an existing case: choose a new id instead. ' +
|
|
151
174
|
'Put the cases of one area in a group folder (cases/<group>/<name>.md, id <group>/<name>), reusing an existing folder when it fits. ' +
|
|
152
175
|
'Write the cases, run pnpm case:check, and list the open questions in your final report.';
|
|
153
|
-
export const makeCaseInvocation = (tool, memoPath) => invocation(tool, makeCasePrompt(memoPath), 'cases-new');
|
|
154
|
-
export const makeCaseCommandLine = (tool, memoPath) => commandLine(makeCaseInvocation(tool, memoPath));
|
|
176
|
+
export const makeCaseInvocation = (tool, memoPath, model, thinking) => invocation(tool, makeCasePrompt(memoPath), 'cases-new', undefined, model, thinking);
|
|
177
|
+
export const makeCaseCommandLine = (tool, memoPath, model, thinking) => commandLine(makeCaseInvocation(tool, memoPath, model, thinking));
|
|
@@ -33,6 +33,8 @@ export interface JobInfo {
|
|
|
33
33
|
aiCommand?: AiCommand;
|
|
34
34
|
/** The conversation of an AI run; a later job can resume it. */
|
|
35
35
|
sessionId?: string;
|
|
36
|
+
/** The model the AI run was started with; omitted = the CLI's own choice. */
|
|
37
|
+
aiModel?: string;
|
|
36
38
|
/** What the agent said in its last `RESULT:` line; null when it did not say or the run did not succeed. */
|
|
37
39
|
resultState?: ResultState | null;
|
|
38
40
|
/** The earlier job whose conversation this job continues. */
|
|
@@ -47,10 +49,33 @@ export interface AwaitingUserInfo {
|
|
|
47
49
|
}
|
|
48
50
|
/** `needs-input`: the agent wrote open questions into the case file and cannot go on without answers. */
|
|
49
51
|
export type ResultState = 'ready' | 'needs-input';
|
|
52
|
+
/**
|
|
53
|
+
* How one block of an AI job's log is presented. Absent for the plain output of every other job, which stays raw.
|
|
54
|
+
* `markdown`: what the agent said; `reasoning`: what it was thinking (not a result); `tool`: one call, started or
|
|
55
|
+
* finished; `result`: the first lines of a call's output; `error` / `hint`: what failed and what to do about it;
|
|
56
|
+
* `meta`: anything else a person should read (the waiting line, noise the CLI printed).
|
|
57
|
+
*/
|
|
58
|
+
export type JobChunkKind = 'tool' | 'result' | 'reasoning' | 'markdown' | 'error' | 'hint' | 'meta';
|
|
59
|
+
/** A tool call of an AI job, so the log can color its name, its outcome and the time it took. */
|
|
60
|
+
export interface JobChunkTool {
|
|
61
|
+
name: string;
|
|
62
|
+
/** What the call is about (the file, the command, the intent); empty when the CLI gave nothing telling. */
|
|
63
|
+
detail: string;
|
|
64
|
+
ok: boolean;
|
|
65
|
+
/** Still running: Claude reports the start of a call, OpenCode only its end. */
|
|
66
|
+
pending?: boolean;
|
|
67
|
+
/** How long it ran, as a person reads it (`0.4 s`); absent when the CLI did not report a start time. */
|
|
68
|
+
took?: string;
|
|
69
|
+
}
|
|
50
70
|
export interface JobChunk {
|
|
51
71
|
seq: number;
|
|
52
72
|
stream: 'stdout' | 'stderr' | 'system';
|
|
53
73
|
data: string;
|
|
74
|
+
/** Set for the blocks of an AI job (see `JobChunkKind`); absent for a plain command's output. */
|
|
75
|
+
kind?: JobChunkKind;
|
|
76
|
+
/** When the block was produced, for AI jobs: shown as its own styled timestamp, not as part of the text. */
|
|
77
|
+
at?: string;
|
|
78
|
+
tool?: JobChunkTool;
|
|
54
79
|
}
|
|
55
80
|
export type StartJobRequest = {
|
|
56
81
|
action: 'signin';
|
|
@@ -68,6 +93,16 @@ export type StartJobRequest = {
|
|
|
68
93
|
env: string;
|
|
69
94
|
force?: boolean;
|
|
70
95
|
attended?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Show the agent's reasoning in the log (`--thinking` for OpenCode; Claude shows it when its model has it on).
|
|
98
|
+
* Off by default: it costs tokens and time.
|
|
99
|
+
*/
|
|
100
|
+
thinking?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* The model to run the agent with (as the CLI names it). Omitted = the CLI's own choice, which for OpenCode is
|
|
103
|
+
* its built-in free model. `heal-spec` keeps the model of the run it continues.
|
|
104
|
+
*/
|
|
105
|
+
model?: string;
|
|
71
106
|
/** `heal-spec` only: the profiles to heal; omitted = every profile that is logged in. */
|
|
72
107
|
profiles?: string[];
|
|
73
108
|
/** `heal-spec` only: what the user knows (optional). The server stores it as a file for the agent. */
|
|
@@ -82,6 +117,8 @@ export type StartJobRequest = {
|
|
|
82
117
|
action: 'make-cases';
|
|
83
118
|
tool: AiTool;
|
|
84
119
|
memo: string;
|
|
120
|
+
model?: string;
|
|
121
|
+
thinking?: boolean;
|
|
85
122
|
} | {
|
|
86
123
|
action: 'run';
|
|
87
124
|
env: string;
|
|
@@ -163,6 +200,16 @@ export interface RunPlan {
|
|
|
163
200
|
}
|
|
164
201
|
export type ProfileStatus = 'missing' | 'unverified' | 'ok' | 'expired' | 'error';
|
|
165
202
|
/** One profile: a row of the Profiles page. */
|
|
203
|
+
export interface SessionCheckInfo {
|
|
204
|
+
/** Where a visitor without a valid session is sent: a path on the site (`/login`) or a full URL (single sign-on). */
|
|
205
|
+
loggedOutUrl: string;
|
|
206
|
+
/** A selector that only exists while signed in; null when the profile does not name one. */
|
|
207
|
+
loggedInSelector: string | null;
|
|
208
|
+
/** A selector that proves the visitor is signed out, e.g. the password field of a login form. */
|
|
209
|
+
loggedOutSelector: string | null;
|
|
210
|
+
/** How long to wait for one of those signals, in milliseconds. */
|
|
211
|
+
timeoutMs: number;
|
|
212
|
+
}
|
|
166
213
|
export interface ProfileInfo {
|
|
167
214
|
id: string;
|
|
168
215
|
name: string;
|
|
@@ -170,6 +217,14 @@ export interface ProfileInfo {
|
|
|
170
217
|
baseURL: string;
|
|
171
218
|
account: string | null;
|
|
172
219
|
tags: string[];
|
|
220
|
+
/** The page "Visit" opens after a login, e.g. `/app/home`; empty = the site root. */
|
|
221
|
+
startPath: string;
|
|
222
|
+
/** A page only a signed-in visitor sees; `/account/` unless set in profiles.yaml. */
|
|
223
|
+
sessionCheckPath: string;
|
|
224
|
+
/** How that page shows a missing login. */
|
|
225
|
+
sessionCheck: SessionCheckInfo;
|
|
226
|
+
/** Tests may send real email in this environment. Always false for production. */
|
|
227
|
+
email: boolean;
|
|
173
228
|
status: ProfileStatus;
|
|
174
229
|
verifying: boolean;
|
|
175
230
|
/** When the session file was last written. */
|
|
@@ -186,11 +241,41 @@ export interface AddProfileRequest {
|
|
|
186
241
|
/** A label for humans (a mailbox or username). Never a password. */
|
|
187
242
|
account?: string;
|
|
188
243
|
tags?: string[];
|
|
244
|
+
/** The page "Visit" opens after a login. Defaults to the site root. */
|
|
245
|
+
startPath?: string;
|
|
189
246
|
/** Page that redirects to /login when logged out. Defaults to /account/. */
|
|
190
247
|
sessionCheckPath?: string;
|
|
248
|
+
/** How that page shows a missing login. Omitted keys keep their default. */
|
|
249
|
+
sessionCheck?: Partial<SessionCheckInfo>;
|
|
191
250
|
/** Only for environments other than production. Defaults to false. */
|
|
192
251
|
email?: boolean;
|
|
193
252
|
}
|
|
253
|
+
/** Changes to one existing profile. The id itself cannot change: commands, session files and baselines use it. */
|
|
254
|
+
export interface EditProfileRequest {
|
|
255
|
+
name: string;
|
|
256
|
+
/** The environment this profile points at, e.g. `production`, `staging`. */
|
|
257
|
+
env: string;
|
|
258
|
+
baseURL: string;
|
|
259
|
+
/** A label for humans (a mailbox or username). Never a password. */
|
|
260
|
+
account?: string;
|
|
261
|
+
tags?: string[];
|
|
262
|
+
/** The page "Visit" opens after a login. Defaults to the site root. */
|
|
263
|
+
startPath?: string;
|
|
264
|
+
/** Page that redirects to /login when logged out. Defaults to /account/. */
|
|
265
|
+
sessionCheckPath?: string;
|
|
266
|
+
/** How that page shows a missing login. Omitted keys keep their default. */
|
|
267
|
+
sessionCheck?: Partial<SessionCheckInfo>;
|
|
268
|
+
/** Only for environments other than production. Defaults to false. */
|
|
269
|
+
email?: boolean;
|
|
270
|
+
}
|
|
271
|
+
export interface EditProfileResult {
|
|
272
|
+
profile: ProfileInfo;
|
|
273
|
+
/**
|
|
274
|
+
* True when the site address changed: the saved login belonged to the old site, was deleted, and the
|
|
275
|
+
* profile has to log in again.
|
|
276
|
+
*/
|
|
277
|
+
sessionCleared: boolean;
|
|
278
|
+
}
|
|
194
279
|
export interface ProfilesResponse {
|
|
195
280
|
profiles: ProfileInfo[];
|
|
196
281
|
/** Every environment used in profiles.yaml, `production` first. */
|
|
@@ -255,3 +340,32 @@ export interface CaseImpact {
|
|
|
255
340
|
snapshotDir: string | null;
|
|
256
341
|
snapshotCount: number;
|
|
257
342
|
}
|
|
343
|
+
/** One model an AI CLI can be run with, as the CLI itself names it. */
|
|
344
|
+
export interface AiModelOption {
|
|
345
|
+
/** Exactly what `--model` takes: `provider/model` or `provider/model#variant` (OpenCode), an alias or an id (Claude). */
|
|
346
|
+
id: string;
|
|
347
|
+
label: string;
|
|
348
|
+
/** Where the row comes from: a provider id, or `alias` / `settings` / `gateway` for a Claude CLI. */
|
|
349
|
+
group: string;
|
|
350
|
+
/** A second line: what the model is, or what an alias resolves to. */
|
|
351
|
+
detail?: string;
|
|
352
|
+
}
|
|
353
|
+
/** The models one AI CLI offers on this machine, read from the CLI itself or from its configuration. */
|
|
354
|
+
export interface AiModelsResponse {
|
|
355
|
+
tool: AiTool;
|
|
356
|
+
models: AiModelOption[];
|
|
357
|
+
/**
|
|
358
|
+
* What the CLI would run when asrun passes no `--model`; null when it cannot be known (OpenCode picks a built-in
|
|
359
|
+
* free model, which is not stored anywhere).
|
|
360
|
+
*/
|
|
361
|
+
clientDefault: string | null;
|
|
362
|
+
/**
|
|
363
|
+
* What to preselect while the user has not chosen: the CLI's default (Claude), or the model the CLI's own last
|
|
364
|
+
* session used (OpenCode). Null when neither is known.
|
|
365
|
+
*/
|
|
366
|
+
suggested: string | null;
|
|
367
|
+
/** Parts of the list that could not be read, in the words of the CLI: shown above the list, never fatal. */
|
|
368
|
+
notes: string[];
|
|
369
|
+
/** Set when there is no list at all (the CLI is not installed, or its answer could not be read). */
|
|
370
|
+
error: string | null;
|
|
371
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Case document contract (for AI agents)
|
|
2
|
+
|
|
3
|
+
This file describes the shape that `lib/case-doc.ts`, `lib/values.ts` and `lib/targets.ts` read, and how a generated
|
|
4
|
+
spec consumes it. It holds no rules of its own: the rules are in the framework `AGENTS.md`, in the skill, and in the
|
|
5
|
+
references listed at the end.
|
|
6
|
+
|
|
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.
|
|
9
|
+
|
|
10
|
+
## Identity and paths
|
|
11
|
+
|
|
12
|
+
- A case id is its path below `cases/` without `.md`: `header-logo`, or `documents/landing-page` with **one** group
|
|
13
|
+
folder (`^[a-z0-9]+(-[a-z0-9]+)*(/[a-z0-9]+(-[a-z0-9]+)*)?$`).
|
|
14
|
+
- The spec is at the same path below `tests/` (`tests/documents/landing-page.spec.ts`), with its `*-snapshots/`
|
|
15
|
+
folder next to it.
|
|
16
|
+
- `cases/_template.md` and names starting with `_` are not cases.
|
|
17
|
+
- Specs import the framework by package name (`@asrunjs/asrun/*`), so a group folder changes no import.
|
|
18
|
+
|
|
19
|
+
## Fixed tokens
|
|
20
|
+
|
|
21
|
+
The parser and the web panel match these literals. A project language never translates them:
|
|
22
|
+
|
|
23
|
+
- Section headings: `## Goal`, `## Steps`, `## Expected`, `## Values`, `## Visual checks`, `## Targets`, `## Notes`,
|
|
24
|
+
and `## Source` when the case was made from notes.
|
|
25
|
+
- Front matter keys: `id`, `title`, `login`, `suites`, `writes`, `writes-note`, `envs`.
|
|
26
|
+
- Step tags at the end of a step line: `[input]`, `[writes]`, `[email]`.
|
|
27
|
+
- Under a step: `- Ready when:` lines and `- Recipe (phase):` lines.
|
|
28
|
+
- Target cards: `### T<n> <name>` and their references `[T<n>]`.
|
|
29
|
+
- Questions: `🚫 TODO [blocking]`, `❓ TODO [non-blocking]`, `✍️ Answer:`, and the markers
|
|
30
|
+
`🔍 to be confirmed by QA` / `👁️ not observed`.
|
|
31
|
+
- The last line of a run: `RESULT: ready` or `RESULT: needs-input`.
|
|
32
|
+
|
|
33
|
+
## Front matter
|
|
34
|
+
|
|
35
|
+
`---` on the first line, YAML, `---` to close. `pnpm case:check` calls `parseCaseMeta`, which refuses the file when
|
|
36
|
+
`suites`, `writes` or `envs` break the rules below. `id`, `title` and `login` are required by convention: keep them
|
|
37
|
+
present (the editor shows them, and the generator reads the title and the login mode), but `case:check` does not
|
|
38
|
+
enforce them.
|
|
39
|
+
|
|
40
|
+
| Key | Required | Rule |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `id` | yes | The case id; the editor keeps it in step with the path. |
|
|
43
|
+
| `title` | yes | One line. |
|
|
44
|
+
| `login` | no | `required` (default) or `none`. `none` → the spec sets `test.use({mustLogin: false})`. |
|
|
45
|
+
| `suites` | yes | Non-empty list of lowercase words. Missing `suites` is an error. |
|
|
46
|
+
| `writes` | no | List of environments. Omit for read-only. Any value other than a known environment is refused; the old `writes: test-data` is refused with a hint. |
|
|
47
|
+
| `writes-note` | with `writes` | Non-empty text: what the case creates or changes. |
|
|
48
|
+
| `envs` | no | List of environments; every environment in `writes` must also be in `envs`. |
|
|
49
|
+
|
|
50
|
+
`priority: <level>` is the old name of `suites`: it is read as `suites: [<level>]`, and naming both is an error.
|
|
51
|
+
Unknown environments are refused against the environments of `profiles.yaml`.
|
|
52
|
+
|
|
53
|
+
`caseTags(id)` (`@asrunjs/asrun/cases`) turns the front matter into Playwright tags: one `@<suite>` per suite, plus
|
|
54
|
+
`@writes` for a case that writes. The spec passes `{tag: caseTags('<id>')}` to the top-level `test.describe`; it never
|
|
55
|
+
hard-codes tags.
|
|
56
|
+
|
|
57
|
+
## Sections
|
|
58
|
+
|
|
59
|
+
### Steps
|
|
60
|
+
|
|
61
|
+
- Numbered Markdown lines (`1.`, `2.`, ...). The spec wraps each one in `test.step('<n>. <text>', ...)` with the text
|
|
62
|
+
verbatim, including the number. Do not paraphrase it.
|
|
63
|
+
- A step's tags are the last tokens of its line: `[input]` (a person makes a choice), `[writes]` (it changes data),
|
|
64
|
+
`[email]` (it sends email). All may appear.
|
|
65
|
+
- `- Ready when:` is a sub-line of the step: the positive signal that what the step loads is there. The spec waits
|
|
66
|
+
for it before reading; it is not an assertion.
|
|
67
|
+
- `- Recipe (phase):` is a sub-line of an `[input]`/`[writes]` step: what a person did in the browser during an
|
|
68
|
+
attended exploration (positions, `[asrun]` names, control labels; no selectors). A tagged step with no recipe is a
|
|
69
|
+
`needs-input` stop for `/gen-spec`.
|
|
70
|
+
- A question block is indented under the sentence it belongs to: 4 spaces under a numbered step, 2 under a bullet.
|
|
71
|
+
The parent line is the suggestion, written as a finished sentence; the `TODO` line holds one question and optional
|
|
72
|
+
`(evidence)`; the block ends with `- ✍️ Answer:` at one level deeper and that line is the last line of the block.
|
|
73
|
+
Details: `.claude/skills/exploring-cases/references/open-questions.md`.
|
|
74
|
+
|
|
75
|
+
### Expected
|
|
76
|
+
|
|
77
|
+
A bullet list of behaviour that must hold for every profile. Profile differences are values, not prose.
|
|
78
|
+
|
|
79
|
+
### Values
|
|
80
|
+
|
|
81
|
+
One fenced `yaml` block, and nothing else in `## Values`:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
default:
|
|
85
|
+
createButton: Create New
|
|
86
|
+
example-staging:
|
|
87
|
+
previewLogoAlt: Example
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- The top level is a map of profile id (or `default`) to a map of keys. Any other shape, an unknown profile id, or
|
|
91
|
+
a value that is not a map is an error.
|
|
92
|
+
- `requireValues('<id>', profile)` deep-merges `default` under the profile's own entry.
|
|
93
|
+
- A profile with no entry and no `default` fails with "Missing values ...". An empty value or one that starts with
|
|
94
|
+
`TODO` fails with "Incomplete values ...". Fill them before running; never make a spec read the YAML itself.
|
|
95
|
+
- Values hold literals only: names, labels, options, test-data names. The checks themselves stay in `## Steps` and
|
|
96
|
+
`## Expected`, and selectors come from `## Targets`.
|
|
97
|
+
|
|
98
|
+
### Visual checks
|
|
99
|
+
|
|
100
|
+
Bullets naming what must match a screenshot baseline. A check on a logo, image, icon, file, PDF, screenshot or
|
|
101
|
+
repeated text needs a `[T<n>]` card.
|
|
102
|
+
|
|
103
|
+
### Targets
|
|
104
|
+
|
|
105
|
+
One card per distinct thing a check looks at:
|
|
106
|
+
|
|
107
|
+
```markdown
|
|
108
|
+
### T1 Header logo
|
|
109
|
+
- What: <the thing itself, and what it is not when a look-alike exists>
|
|
110
|
+
- Where: <region of the page, dialog, tab or file page>
|
|
111
|
+
- Identified by: <role and name, test id, alt text or title; never a position>
|
|
112
|
+
- Not: <look-alikes that must not be matched>
|
|
113
|
+
- Ready when: <what shows that the content has loaded>
|
|
114
|
+
- Reference <profile-id>: .explore/targets/<case-id>/T1-<profile-id>.png
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `What`, `Where` and `Identified by` are required for a card that is not blocking.
|
|
118
|
+
- `Reference <profile-id>:` must point under `.explore/targets/<case-id>/` (git-ignored), or be `none: <reason>`.
|
|
119
|
+
A missing file on this machine is a warning, not a failure: a fresh checkout has the card but not the screenshot.
|
|
120
|
+
- A card may hold a `🚫 TODO [blocking]` block. Such a card is not complete; `pnpm case:check` does not fail on it,
|
|
121
|
+
and the check that uses it is not generated yet.
|
|
122
|
+
- Every `[T<n>]` in a step, a ready signal or a check resolves to a card. `pnpm case:check` fails on a reference with
|
|
123
|
+
no card.
|
|
124
|
+
- The binding rules for choosing a target (never guess, list candidates) are in
|
|
125
|
+
`.claude/skills/exploring-cases/references/assertion-targets.md`.
|
|
126
|
+
|
|
127
|
+
### Notes and Source
|
|
128
|
+
|
|
129
|
+
`## Notes` is free prose for a reviewer. `## Source` holds the original lines of a memo, written by `/make-case`.
|
|
130
|
+
The panel shows both read-only.
|
|
131
|
+
|
|
132
|
+
## How a spec consumes the case
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// case: cases/<id>.md
|
|
136
|
+
import {caseTags} from '@asrunjs/asrun/cases';
|
|
137
|
+
import {configureCase, expect, test} from '@asrunjs/asrun/fixtures';
|
|
138
|
+
import {requireValues} from '@asrunjs/asrun/values';
|
|
139
|
+
|
|
140
|
+
test.describe('<Case title>', {tag: caseTags('<id>')}, () => {
|
|
141
|
+
configureCase('<id>'); // no retries and a longer timeout for a case that writes data
|
|
142
|
+
test('<single outcome>', async ({page, profile}) => {
|
|
143
|
+
const expected = requireValues<{/* keys */}>('<id>', profile);
|
|
144
|
+
|
|
145
|
+
await test.step('1. <step text, verbatim>', async () => {
|
|
146
|
+
// an action that loads content: actAndWait(page, () => click(), {responses, ready})
|
|
147
|
+
// a confirmation that may appear: clickOptionalUntilHidden(confirm, target, {message})
|
|
148
|
+
// a download: verifyDownload(page, trigger, {kind})
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- The first line `// case: cases/<id>.md` is what `pnpm spec:lint` uses to find the case and its targets.
|
|
155
|
+
- `// target: T<n>` goes on the line before an image/screenshot/PDF check. `// ready: <signal>` goes before a check
|
|
156
|
+
for absence ("missing", "empty", "hidden") and needs a positive wait before it. `// pages: <the case's words>` goes
|
|
157
|
+
before an `expectPdf` that follows a case limit written in strong words ("only", "exactly", "must").
|
|
158
|
+
- Import `test`/`expect` from `@asrunjs/asrun/fixtures`, never from `@playwright/test`. Locators:
|
|
159
|
+
`getByTestId` > `getByRole` > `getByLabel` > text.
|
|
160
|
+
- No branching on `profile.id` or `profile.env`: differences go into `## Values`, selectors into `## Targets`.
|
|
161
|
+
- Paths are relative (`page.goto('/account/')`); the Playwright project sets `baseURL`.
|
|
162
|
+
|
|
163
|
+
## Profiles, values and the write gate
|
|
164
|
+
|
|
165
|
+
- Each entry in `profiles.yaml` is one Playwright project named after its `id`. A spec runs once per profile.
|
|
166
|
+
- The `profile` fixture exposes `id`, `env`, `baseURL`, `account`, `tags`, `email`.
|
|
167
|
+
- The `caseGate` fixture enforces `envs` and `writes` at run time, also when `playwright test` is called directly:
|
|
168
|
+
a case limited to some environments is skipped elsewhere; a case that writes elsewhere only is skipped with a
|
|
169
|
+
notice; a write in a non-staging environment needs the confirmation the CLI sets, never an environment variable
|
|
170
|
+
that was merely present.
|
|
171
|
+
- Write cases: name everything `[asrun] ...`, register it with `records.track` right after creating it, remove it
|
|
172
|
+
newest first (also after a failure), turn retries off with `configureCase`, and keep email steps behind
|
|
173
|
+
`emailStepAllowed(profile, '<step title>')`.
|
|
174
|
+
|
|
175
|
+
## Checks to run before reporting
|
|
176
|
+
|
|
177
|
+
| Command | Must |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `pnpm case:check [<id>]` | no `FAIL` |
|
|
180
|
+
| `pnpm typecheck` | no errors |
|
|
181
|
+
| `pnpm spec:lint <id>` | no `FAIL` |
|
|
182
|
+
| `pnpm baselines:review <id>` | every new image is looked at (a person's job) |
|
|
183
|
+
|
|
184
|
+
## Related references
|
|
185
|
+
|
|
186
|
+
In the framework package:
|
|
187
|
+
|
|
188
|
+
- `.claude/skills/exploring-cases/references/assertion-targets.md` — what a check looks at, and when to ask.
|
|
189
|
+
- `.claude/skills/exploring-cases/references/readiness.md` — wait for the content, optional confirmations, downloads.
|
|
190
|
+
- `.claude/skills/exploring-cases/references/open-questions.md` — question blocks and how answers are folded in.
|
|
191
|
+
- `.claude/skills/exploring-cases/references/user-handoff.md` — attended exploration and the handoff card.
|
|
192
|
+
- `.claude/skills/generating-specs/references/conventions.md` — spec shape, assertion layers, readable failures.
|
|
193
|
+
- `docs/cases.md` — the same format for people writing a case by hand.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Working on AsRun itself
|
|
2
|
+
|
|
3
|
+
Read `AGENTS.md` first: it is the product baseline, and this file only adds how to change the framework. Nothing here
|
|
4
|
+
is relevant to a project that uses asrun as a dependency.
|
|
5
|
+
|
|
6
|
+
## Framework layout
|
|
7
|
+
|
|
8
|
+
`<framework>` for a project is `node_modules/@asrunjs/asrun`. In this repository it is the root folder:
|
|
9
|
+
|
|
10
|
+
- `lib/` fixtures (`profile`, session guard, write gate) and helpers; specs import them as
|
|
11
|
+
`@asrunjs/asrun/<module>` (for example `@asrunjs/asrun/fixtures`, `@asrunjs/asrun/values`)
|
|
12
|
+
- `scripts/` the user-facing commands; every command and flag is documented in `docs/commands.md`
|
|
13
|
+
- `templates/` the files `pnpm asrun init` writes into a workspace
|
|
14
|
+
- `ui/` local web UI (`pnpm ui`). It may only run the fixed actions in `ui/server/actions.ts`
|
|
15
|
+
and must never serve anything under `.auth/`.
|
|
16
|
+
- `docs/` user documentation (`install.md`, `profiles.md`, `cases.md`, `ai-workflow.md`,
|
|
17
|
+
`commands.md`, `web-ui.md`, `troubleshooting.md`) and `docs/theme-guide.html`
|
|
18
|
+
- `docs/agents/` documents for AI agents: this file, and `case-format.md` (the case document contract)
|
|
19
|
+
- `examples/demo/` a workspace for developing the framework itself against a local Docker stack (Conduit);
|
|
20
|
+
it is committed (`.auth/` and `reports/` never are), see its README
|
|
21
|
+
- `.claude/` skills + slash commands for Claude Code; the skills are shared with OpenCode
|
|
22
|
+
- `.opencode/commands/` the same commands for OpenCode (it does not read `.claude/commands`)
|
|
23
|
+
- `AGENTS.md` the product baseline rules (OpenCode reads it); `CLAUDE.md` imports it for Claude Code
|
|
24
|
+
- `README.md` the user-facing overview: what it does, install, first test, cookbook, links to `docs/`
|
|
25
|
+
|
|
26
|
+
`package.json` `files` ships `README.md`, `AGENTS.md`, `CLAUDE.md`, `docs/`, `templates/`, `.claude/`, `bin/`,
|
|
27
|
+
`dist/` and `ui/dist/`, so a project can read the same documentation from
|
|
28
|
+
`node_modules/@asrunjs/asrun/docs/`.
|
|
29
|
+
|
|
30
|
+
## Working on the framework itself
|
|
31
|
+
|
|
32
|
+
- The framework repository holds no workspace content at the root: no `profiles.yaml`, `cases/`, `tests/` or
|
|
33
|
+
reports there. The one workspace is `examples/demo`, which is committed because it runs its own local site; its
|
|
34
|
+
`.auth/`, `reports/` and other run output are not.
|
|
35
|
+
- Develop against the demo:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm demo:up # build and start the local stack
|
|
39
|
+
pnpm demo:seed # accounts, articles, follows, favorites
|
|
40
|
+
pnpm asrun init --root examples/demo # also writes the framework's .env (ASRUN_ROOT=examples/demo)
|
|
41
|
+
pnpm build # the demo's specs import the package, which resolves to dist/
|
|
42
|
+
pnpm asrun --env staging --read-only
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`pnpm demo:reset` deletes the database and seeds it again; logins must be redone afterwards. Demo accounts are in
|
|
46
|
+
`examples/demo/README.md`.
|
|
47
|
+
- Everything the framework reads for a project goes through `ROOT` in `lib/paths.ts` (the workspace root); its own
|
|
48
|
+
assets (templates, docs, skills, its `node_modules`) go through `FRAMEWORK_ROOT`. Never write into
|
|
49
|
+
`FRAMEWORK_ROOT` at run time.
|
|
50
|
+
- After changing `lib/`, run `pnpm build` before exercising the demo's specs.
|
|
51
|
+
- `templates/` is the source of the files a workspace gets. When a template changes, bump `version` in
|
|
52
|
+
`package.json` so `pnpm asrun doctor` in a project shows that it is behind.
|
|
53
|
+
- `init` owns two lists: `GENERATED` (rewritten by `--update`; the commands and the Playwright files) and `ONCE`
|
|
54
|
+
(written only when missing; `AGENTS.md`, `CLAUDE.md`, the configs). `AGENTS.md`, `CLAUDE.md` and
|
|
55
|
+
`.claude/skills/**` belong to the project: `init --force` refuses them unless `--reset-rules` is given, so a
|
|
56
|
+
framework update can never overwrite a project's rules.
|
|
57
|
+
- A project's rules never flow back here. The framework reads a workspace only through `ROOT` at run time; nothing
|
|
58
|
+
under `templates/` or `.claude/skills/` is generated from a project.
|
|
59
|
+
|
|
60
|
+
### Installing this repository as a Git dependency (private)
|
|
61
|
+
|
|
62
|
+
The npm package is the public channel for users. This repository is private, so a Git dependency works only on a
|
|
63
|
+
machine that can clone it:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pnpm add -D github:wizcas/asrun#<tag>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
pnpm 12 resolves a GitHub spec through the canonical HTTPS URL, not SSH. That machine therefore needs HTTPS
|
|
70
|
+
credentials (a credential helper or a token), or a rewrite to SSH:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git config --global url."git@github.com:".insteadOf https://github.com/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
On CI, give the runner a `GITHUB_TOKEN`, for example:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git config --global \
|
|
80
|
+
url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
or a deploy key with the SSH rewrite above. A Git install also runs the package's `prepare` (it builds `dist/` and
|
|
84
|
+
`ui/dist/`), so it is slower than the registry: use it only to pin a commit, a branch or a tag that is not published
|
|
85
|
+
yet. For a one-off change, `pnpm patch @asrunjs/asrun` and commit the patch instead.
|
|
86
|
+
|
|
87
|
+
## Documentation
|
|
88
|
+
|
|
89
|
+
The split is intentional; keep it:
|
|
90
|
+
|
|
91
|
+
- `README.md` — the user's first page: what AsRun does, requirements, install, the first test, customizing the
|
|
92
|
+
prompts, the every-day cookbook, safety in short, a documentation index, maintainer notes. It should stay readable
|
|
93
|
+
in one sitting; a section that grows past a screen belongs in `docs/`.
|
|
94
|
+
- `docs/*.md` — the user reference. `docs/commands.md` is the only place that lists every command and flag.
|
|
95
|
+
- `docs/agents/*.md` — for AI agents. `case-format.md` is the contract of the case document (fixed tokens, values,
|
|
96
|
+
targets); `AGENTS.md` references it, and the template `AGENTS.md` does too.
|
|
97
|
+
|
|
98
|
+
When behavior changes, update the matching document in the same change. A command or flag goes into
|
|
99
|
+
`docs/commands.md` and, if it is a daily one, into the README cookbook. A change to the case format goes into
|
|
100
|
+
`docs/agents/case-format.md` and `docs/cases.md` together. Keep the relative links working and the fixed tokens
|
|
101
|
+
spelled exactly as the parser reads them.
|
|
102
|
+
|
|
103
|
+
## The commands and the two agent CLIs
|
|
104
|
+
|
|
105
|
+
- Claude Code reads `.claude/commands/*` and `.claude/skills/<name>/SKILL.md`.
|
|
106
|
+
- OpenCode reads `.opencode/commands/*`, `.claude/skills/<name>/SKILL.md`, `.opencode/skills/<name>/SKILL.md` and
|
|
107
|
+
`.agents/skills/<name>/SKILL.md`. It does **not** read `.claude/commands`, and it does **not** read a flat
|
|
108
|
+
`.claude/skills/<skill>.md` as a skill (that file is a project override, read by the command and by the skill).
|
|
109
|
+
- `GENERATED` renders the same four command texts into both `.claude/commands/` and `.opencode/commands/`, so edit
|
|
110
|
+
`templates/.claude/commands/*.md` once. In this repository the eight command files are written by hand: keep
|
|
111
|
+
`.claude/commands/` and `.opencode/commands/` identical (`diff -r .claude/commands .opencode/commands`).
|
|
112
|
+
- OpenCode discovers skills by walking up from the working directory to the git worktree, so a workspace sees only
|
|
113
|
+
its own skills, not the framework ones under `node_modules`: the command's `Load the framework skill <path>` is a
|
|
114
|
+
plain file read. That is why the skill itself must read the project override (`.claude/skills/<skill>.md`).
|
|
115
|
+
|
|
116
|
+
## Web UI (`ui/web`)
|
|
117
|
+
|
|
118
|
+
Before adding or changing any UI element, read `docs/theme-guide.html` (open the file; it is static HTML) and follow it.
|
|
119
|
+
Its rules are binding, in particular:
|
|
120
|
+
|
|
121
|
+
- Icons: Feather via the `react-feather` package only (`import {ChevronDown} from 'react-feather'`). Never draw, paste
|
|
122
|
+
or hand-write SVGs, and never add another icon library. Size 14 in buttons, 16-20 elsewhere; decorative icons get
|
|
123
|
+
`aria-hidden="true"`, icon-only buttons get an `aria-label`. Do not use Unicode symbols (`▲ ▼ ◀ ▶ ⤢`) as icons.
|
|
124
|
+
- Colors: only the tokens in `ui/web/src/index.css` (the default Tailwind palette is removed). Pick the color by impact.
|
|
125
|
+
- Buttons and badges: use `buttonClass()` and `Badge` from `ui/web/src/components/status.tsx`, never hand-style them.
|
|
126
|
+
- Dropdowns: use `Select` from the same file, never a bare `<select>` or hand-copied classes (native arrow and height
|
|
127
|
+
differ). A required dropdown that is empty gets `invalid` (red). See the "Dropdowns" section of the theme guide.
|
|
128
|
+
- A list the user has to search in (a case id, a file name) is `SearchSelect` from
|
|
129
|
+
`ui/web/src/components/search-select.tsx`: an input with a filtered list below it, which a native `<select>` cannot
|
|
130
|
+
do. It keeps `Select`'s height, border and focus ring, so the two look like one family; do not build a third variant.
|
|
131
|
+
- If a change adds or alters a token, update `ui/web/src/index.css` and `docs/theme-guide.html` together.
|
|
132
|
+
- The Help page ("How to start") shows screenshots from `ui/web/public/help/`: retake one when the page it shows
|
|
133
|
+
changes.
|
|
134
|
+
|
|
135
|
+
Before reaching for a new dependency or writing a helper component, grep `package.json` and `ui/web/src` for what the
|
|
136
|
+
project already uses.
|
|
137
|
+
|
|
138
|
+
### Copy (`ui/web/src`)
|
|
139
|
+
|
|
140
|
+
Every lead, label and tooltip is written for the person using the UI, never for the person who built it. Four rules:
|
|
141
|
+
|
|
142
|
+
- **What → what you do → next.** A page, panel or section explains three things, in that order: what it does, what the
|
|
143
|
+
user has to do, and what comes next (state it only when something does). A run that needs nothing from the user says so.
|
|
144
|
+
- **Short.** A lead is at most three sentences. A longer explanation becomes labeled paragraphs (`<b>While it runs</b> — …`)
|
|
145
|
+
or a bullet list, never one dense block. A tooltip is `# Title`, then at most three lines of one sentence each: it
|
|
146
|
+
wraps inside 320px (`ui/web/src/lib/tooltip.ts`).
|
|
147
|
+
- **No implementation in the copy.** File paths, directory names, CLI flags, skill or command names, and the agent's
|
|
148
|
+
internal syntax (`TODO`, `Answer:`, `RESULT: needs-input`, `- Recipe:`) never appear in a lead, a label or a tooltip.
|
|
149
|
+
They belong where the file, the command or the syntax is the object itself: the Spec view, the Jobs drawer, a
|
|
150
|
+
`CopyBlock`, or a folded `<details>` block titled "For the command line".
|
|
151
|
+
- **Say what the user sees.** "The run appears in the Jobs bar", not "the command runs in a one-shot session"; "the
|
|
152
|
+
browser shows a small card", not "the agent pauses on a `writes` step".
|
|
153
|
+
|
|
154
|
+
## Before you open a pull request
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pnpm typecheck
|
|
158
|
+
pnpm build # dist/ must match lib/ and scripts/
|
|
159
|
+
pnpm case:check # examples/demo cases
|
|
160
|
+
pnpm spec:lint # examples/demo specs
|
|
161
|
+
pnpm ui:build # ui/dist/ must match ui/web and ui/server
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- `pnpm build` and `pnpm ui:build` also run from `prepare` and `prepack`, so a published package is complete.
|
|
165
|
+
- Bump `version` when `templates/` or the shipped docs change; `pnpm asrun doctor` in a project compares it.
|
|
166
|
+
- Keep the demo's committed expectations (`profiles.yaml`, `cases/`, `tests/`, baselines) in step with the demo site.
|
|
167
|
+
Baselines are a person's decision: never let an agent regenerate them.
|