rvlib-shipkit 2.2.0 → 2.3.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/CHANGELOG.md +38 -0
- package/package.json +1 -1
- package/src/agent-time/mask.ts +43 -0
- package/src/agent-time/render.ts +187 -0
- package/src/agent-time/report.ts +335 -0
- package/src/agent-time/shape.ts +271 -0
- package/src/agent-time/signals.ts +84 -0
- package/src/agent-time/store.ts +379 -0
- package/src/agent-time/timeline.ts +218 -0
- package/src/agent-time/turns.ts +135 -0
- package/src/cli/cmd_Commands.ts +5 -0
- package/src/cli/cmd_Review.ts +948 -0
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +259 -6
- package/src/cli/kit/verb.ts +14 -4
- package/src/cli.ts +6 -4
- package/src/collections/collection-types.ts +6 -1
- package/src/collections/providers.ts +3 -0
- package/src/collections/review.ts +70 -0
- package/src/core/layout.ts +2 -0
- package/src/reflect/prompt-corpus.ts +15 -119
- package/src/reflect/transcript.ts +254 -0
- package/src/review/anim.ts +228 -0
- package/src/review/auto.ts +141 -0
- package/src/review/format.ts +150 -0
- package/src/review/http.ts +23 -0
- package/src/review/marks.ts +43 -0
- package/src/review/media.ts +45 -0
- package/src/review/model.ts +854 -0
- package/src/review/project.ts +110 -0
- package/src/review/role.ts +48 -0
- package/src/review/routes.ts +228 -0
- package/src/review/store.ts +1002 -0
- package/src/review/threads.ts +237 -0
- package/src/review/trailer-desk.ts +385 -0
- package/src/review/trailer-model.ts +183 -0
- package/src/review/trailer-routes.ts +136 -0
- package/src/review/unit.ts +165 -0
- package/src/review/watchers.ts +54 -0
- package/src/serve/embed.ts +35 -0
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/activity.css +0 -4
- package/src/serve/hub-app/dock-view.tsx +5 -4
- package/src/serve/hub-app/hash-route.ts +28 -1
- package/src/serve/hub-app/header-bar.tsx +3 -1
- package/src/serve/hub-app/home-st.ts +10 -2
- package/src/serve/hub-app/home-strip.tsx +3 -2
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +80 -11
- package/src/serve/hub-app/hub.css +45 -13
- package/src/serve/hub-app/icons.tsx +9 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/insights-view.tsx +4 -3
- package/src/serve/hub-app/keys.ts +14 -10
- package/src/serve/hub-app/layout-st.ts +63 -2
- package/src/serve/hub-app/planned-view.tsx +3 -2
- package/src/serve/hub-app/planned.css +0 -1
- package/src/serve/hub-app/project-list.css +22 -4
- package/src/serve/hub-app/project-list.tsx +41 -8
- package/src/serve/hub-app/projects-svc.ts +4 -0
- package/src/serve/hub-app/repo-sections.ts +15 -3
- package/src/serve/hub-app/repo-st.ts +45 -3
- package/src/serve/hub-app/review/clip-st.ts +208 -0
- package/src/serve/hub-app/review/kind-view.tsx +171 -0
- package/src/serve/hub-app/review/kit.tsx +113 -0
- package/src/serve/hub-app/review/review-answer.tsx +239 -0
- package/src/serve/hub-app/review/review-board.tsx +46 -0
- package/src/serve/hub-app/review/review-list.tsx +156 -0
- package/src/serve/hub-app/review/review-page.tsx +101 -0
- package/src/serve/hub-app/review/review-st.ts +779 -0
- package/src/serve/hub-app/review/review-views.tsx +503 -0
- package/src/serve/hub-app/review/trailer-page.tsx +416 -0
- package/src/serve/hub-app/review/trailer-st.ts +602 -0
- package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
- package/src/serve/hub-app/review.css +329 -0
- package/src/serve/hub-app/theme-st.ts +56 -0
- package/src/serve/hub-app/tree-build.ts +4 -0
- package/src/serve/hub-app/tree-row.tsx +1 -1
- package/src/serve/hub-app/tree-svc.ts +4 -0
- package/src/serve/hub-runtime.ts +5 -3
- package/src/serve/same-origin.ts +5 -1
- package/src/serve/serve-hub.ts +8 -0
- package/src/shipkit-files.ts +9 -5
- package/src/trailer/adapter.ts +58 -3
- package/src/trailer/build.ts +300 -41
- package/src/trailer/ffmpeg.ts +22 -0
- package/src/trailer/footage.ts +109 -40
- package/src/trailer/grammar.ts +444 -69
- package/src/trailer/graph.ts +199 -14
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +20 -3
- package/src/trailer/look.ts +17 -3
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +80 -21
- package/src/trailer/prune.ts +193 -0
- package/src/trailer/score/compose.ts +240 -0
- package/src/trailer/score/dsp.ts +272 -0
- package/src/trailer/score/palette.ts +188 -0
- package/src/trailer/score/pattern.ts +91 -0
- package/src/trailer/score/score.ts +214 -0
- package/src/trailer/score/sound.ts +112 -0
- package/src/trailer/score/synth.ts +361 -0
- package/src/trailer/states.ts +76 -0
- package/src/trailer/stitch.ts +92 -9
- package/src/trailer/titles.ts +123 -18
- package/src/trailer/vocab.ts +23 -4
- package/src/trailer/words.ts +254 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// A tool call's SHAPE: the name its time is grouped under. A Bash command
|
|
2
|
+
// becomes its program and subcommand (`git commit`, `bun test <file>`,
|
|
3
|
+
// `sk ci --only lint`), so five hundred variants group into a few dozen rows.
|
|
4
|
+
//
|
|
5
|
+
// The constraint a reader breaks first: a shape never keeps a free value (a
|
|
6
|
+
// path, a message, a hash), or every call becomes its own row again. The only
|
|
7
|
+
// value kept is `sk ci --only <check>`, a small closed set that says WHICH
|
|
8
|
+
// check is slow.
|
|
9
|
+
|
|
10
|
+
/** bumped with any rule below: the index shapes every stored call again */
|
|
11
|
+
export const SHAPE_VERSION = 4
|
|
12
|
+
|
|
13
|
+
/** programs whose first word is never a subcommand */
|
|
14
|
+
const NO_SUBCOMMAND = new Set([
|
|
15
|
+
'awk', 'cat', 'chmod', 'cp', 'curl', 'date', 'diff', 'du', 'echo', 'ffmpeg', 'ffprobe', 'file', 'find', 'grep', 'head',
|
|
16
|
+
'jq', 'kill', 'killall', 'less', 'ln', 'ls', 'lsof', 'mkdir', 'mv', 'open', 'pgrep', 'pkill', 'printf', 'ps', 'pwd', 'rg',
|
|
17
|
+
'rm', 'rmdir', 'sed', 'sleep', 'sort', 'sqlite3', 'stat', 'tail', 'tee', 'test', 'touch', 'tr', 'tree', 'uniq', 'uptime',
|
|
18
|
+
'wc', 'wget', 'which', 'xargs', 'perl', 'magick', 'convert', 'zip', 'unzip', 'tar', 'osascript', 'say', 'afplay', 'screencapture', 'sips', 'top',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
/** programs whose shape keeps two subcommand words (`gh pr view`, `sk gp start`) */
|
|
22
|
+
const TWO_LEVELS = new Set(['sk', 'shipkit', 'gh', 'docker', 'kubectl', 'gcloud', 'wrangler', 'chi', 'comfy-ts', 'cflow', 'brew'])
|
|
23
|
+
|
|
24
|
+
/** bun's own subcommands; any other first word is a package script */
|
|
25
|
+
const BUN_BUILTINS = new Set(['test', 'run', 'x', 'install', 'i', 'add', 'remove', 'rm', 'build', 'pm', 'link', 'unlink', 'init', 'create', 'upgrade', 'lint', 'fmt', 'outdated', 'update', 'publish', 'patch', 'why', 'audit', 'exec', 'repl', 'info'])
|
|
26
|
+
|
|
27
|
+
/** a segment that only sets the scene for the next one */
|
|
28
|
+
const SETUP = new Set(['cd', 'export', 'set', 'source', '.', 'pushd', 'popd', 'unset', 'ulimit', 'shopt', 'true', ':'])
|
|
29
|
+
|
|
30
|
+
/** words that run the rest of the line as the real command */
|
|
31
|
+
const WRAPPERS: Record<string, number> = { nice: 0, time: 0, timeout: 1, command: 0, exec: 0, env: 0, sudo: 0, caffeinate: 0, stdbuf: 0, nohup: 0 }
|
|
32
|
+
|
|
33
|
+
/** flags that eat the next word, per program, before its subcommand */
|
|
34
|
+
const ARG_FLAGS: Record<string, Set<string>> = { git: new Set(['-C', '-c']), nice: new Set(['-n']), bun: new Set(['--cwd', '--filter']) }
|
|
35
|
+
|
|
36
|
+
/** shell words, quotes removed; enough of a shell to split a command line */
|
|
37
|
+
export function shellWords(segment: string): string[] {
|
|
38
|
+
const words: string[] = []
|
|
39
|
+
let cur = ''
|
|
40
|
+
let quote: string | null = null
|
|
41
|
+
let any = false
|
|
42
|
+
for (let i = 0; i < segment.length; i++) {
|
|
43
|
+
const c = segment[i] as string
|
|
44
|
+
if (quote != null) {
|
|
45
|
+
if (c === quote) quote = null
|
|
46
|
+
else if (c === '\\' && quote === '"' && i + 1 < segment.length) cur += segment[++i]
|
|
47
|
+
else cur += c
|
|
48
|
+
} else if (c === '"' || c === "'") {
|
|
49
|
+
quote = c
|
|
50
|
+
any = true
|
|
51
|
+
} else if (c === '\\' && i + 1 < segment.length) cur += segment[++i]
|
|
52
|
+
else if (/\s/.test(c)) {
|
|
53
|
+
if (cur !== '' || any) words.push(cur)
|
|
54
|
+
cur = ''
|
|
55
|
+
any = false
|
|
56
|
+
} else cur += c
|
|
57
|
+
}
|
|
58
|
+
if (cur !== '' || any) words.push(cur)
|
|
59
|
+
return words
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** a heredoc's body is data, never a command: `cat > f <<'EOF'` keeps its
|
|
63
|
+
* first line, the lines up to the marker go */
|
|
64
|
+
export function stripHeredocs(command: string): string {
|
|
65
|
+
const out: string[] = []
|
|
66
|
+
let marker: string | null = null
|
|
67
|
+
for (const line of command.split('\n')) {
|
|
68
|
+
if (marker != null) {
|
|
69
|
+
if (line.trim() === marker) marker = null
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
out.push(line)
|
|
73
|
+
const m = /<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/.exec(line)
|
|
74
|
+
if (m?.[1] != null) marker = m[1]
|
|
75
|
+
}
|
|
76
|
+
return out.join('\n')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** the statements of a command line (split on `&&`, `||`, `;` and newlines
|
|
80
|
+
* outside quotes), each one only its first pipeline part: in `grep x | head`
|
|
81
|
+
* the time is the producer's */
|
|
82
|
+
export function statements(command: string): string[] {
|
|
83
|
+
const out: string[] = []
|
|
84
|
+
let cur = ''
|
|
85
|
+
let piped = false
|
|
86
|
+
let quote: string | null = null
|
|
87
|
+
const text = stripHeredocs(command).replace(/\\\n/g, ' ')
|
|
88
|
+
const flush = (): void => {
|
|
89
|
+
out.push(cur)
|
|
90
|
+
cur = ''
|
|
91
|
+
piped = false
|
|
92
|
+
}
|
|
93
|
+
for (let i = 0; i < text.length; i++) {
|
|
94
|
+
const c = text[i] as string
|
|
95
|
+
if (quote != null) {
|
|
96
|
+
if (c === quote) quote = null
|
|
97
|
+
if (!piped) cur += c
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if (c === '"' || c === "'") {
|
|
101
|
+
quote = c
|
|
102
|
+
if (!piped) cur += c
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const two = text.slice(i, i + 2)
|
|
106
|
+
if (two === '&&' || two === '||') {
|
|
107
|
+
flush()
|
|
108
|
+
i++
|
|
109
|
+
} else if (c === ';' || c === '\n') flush()
|
|
110
|
+
else if (c === '|') piped = true
|
|
111
|
+
else if (!piped) cur += c
|
|
112
|
+
}
|
|
113
|
+
flush()
|
|
114
|
+
return out.map((x) => x.trim()).filter((x) => x !== '')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const isWord = (w: string): boolean => /^[a-z][a-z0-9:_-]*$/i.test(w) && !/\.[a-z0-9]+$/i.test(w)
|
|
118
|
+
const isPathLike = (w: string): boolean => w.includes('/') || /\.[a-z0-9]{1,5}$/i.test(w) || w.startsWith('~')
|
|
119
|
+
const isScript = (w: string): boolean => /\.(?:sh|bash|zsh|ts|tsx|js|mjs|cjs|py|rb|pl)$/i.test(w) || (w.includes('/') && !w.startsWith('-'))
|
|
120
|
+
const base = (w: string): string => w.replace(/\/+$/, '').split('/').pop() ?? w
|
|
121
|
+
|
|
122
|
+
/** a statement's words: redirections, a leading `X=$(` and env assignments gone */
|
|
123
|
+
function wordsOf(statement: string): string[] {
|
|
124
|
+
// `cat > f`, `cat a > b`: a file write, whatever it writes
|
|
125
|
+
if (/^cat\b[^|]*?(?<![0-9&>])>{1,2}(?!&)/.test(statement)) return ['cat', '>']
|
|
126
|
+
const w = shellWords(statement.replace(/^[A-Za-z_][A-Za-z0-9_]*=\$\(/, '').replace(/^\(+|\)+$/g, '').replace(/\s\d?>>?&?\d?\s*\S*/g, ' ').replace(/\s<\S+/g, ' '))
|
|
127
|
+
while (w.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(w[0] as string)) w.shift()
|
|
128
|
+
return w
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** statements that cost nothing next to the one they come with: a shape skips
|
|
132
|
+
* them when a heavier statement follows (`uptime; sk ci`, `mkdir -p x && bun y.ts`) */
|
|
133
|
+
const LIGHT = new Set(['echo', 'printf', 'uptime', 'date', 'pwd', 'mkdir', 'touch', 'which', 'clear', 'rm', 'cat'])
|
|
134
|
+
|
|
135
|
+
const isLight = (w: string[]): boolean => LIGHT.has(w[0] as string) && (w[0] !== 'cat' || w.length === 1 || w[1] === '>' || w.every((x, i) => i === 0 || x.startsWith('-')))
|
|
136
|
+
|
|
137
|
+
/** a python heredoc that opens a file to write it: an edit through Bash */
|
|
138
|
+
const PY_EDIT = /\bpython3?\s+-\s*<</
|
|
139
|
+
const PY_WRITES = /open\([^)]*,\s*['"][wa]|\.write(?:_text)?\(/
|
|
140
|
+
|
|
141
|
+
/** the shape of one Bash command line */
|
|
142
|
+
export function commandShape(command: string): string {
|
|
143
|
+
const stmts = statements(command.trim()).map(wordsOf).filter((w) => w.length > 0)
|
|
144
|
+
if (stmts.length === 0) return '(empty)'
|
|
145
|
+
const shape = pick(stmts) ?? (base(stmts[0]?.[0] ?? '') || '(empty)')
|
|
146
|
+
if (shape.startsWith('python') && !shape.includes(' ') && PY_EDIT.test(command) && PY_WRITES.test(command)) return `${shape} (edit)`
|
|
147
|
+
return shape
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** the statement the time belongs to, shaped: loops by what they run */
|
|
151
|
+
function pick(stmts: string[][]): string | null {
|
|
152
|
+
let light: string[] | null = null
|
|
153
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
154
|
+
const w = stmts[i] as string[]
|
|
155
|
+
const head = w[0] as string
|
|
156
|
+
if (head === 'for' || head === 'while' || head === 'until' || head === 'select') return loopShape(head, w, stmts.slice(i + 1))
|
|
157
|
+
if (head === 'if') return '(if)'
|
|
158
|
+
if (/^[A-Za-z_][A-Za-z0-9_-]*\(\)/.test(head) || head === 'function') return '(function)'
|
|
159
|
+
if (SETUP.has(head) || head === 'do' || head === 'done' || head === 'then' || head === 'fi') continue
|
|
160
|
+
if (isLight(w)) {
|
|
161
|
+
light ??= w
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
return shapeOfWords(w)
|
|
165
|
+
}
|
|
166
|
+
return light == null ? null : shapeOfWords(light)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** `(for) bun <file>`: a for loop by its body, a while or until loop by its
|
|
170
|
+
* condition (a `[ … ]` test says nothing, so by its body then) */
|
|
171
|
+
function loopShape(head: string, w: string[], after: string[][]): string {
|
|
172
|
+
const label = head === 'select' ? 'for' : head
|
|
173
|
+
const doAt = after.findIndex((x) => x[0] === 'do')
|
|
174
|
+
const body = doAt === -1 ? [] : [after[doAt]?.slice(1) ?? [], ...after.slice(doAt + 1)].filter((x) => x.length > 0)
|
|
175
|
+
const end = body.findIndex((x) => x[0] === 'done')
|
|
176
|
+
const inner = pick(end === -1 ? body : body.slice(0, end))
|
|
177
|
+
if (label === 'for') return `(for) ${inner ?? '…'}`
|
|
178
|
+
const cond = w.slice(1).filter((x) => x !== '!').map((x, i) => (i === 0 ? x.replace(/^[A-Za-z_][A-Za-z0-9_]*=\$\(/, '') : x))
|
|
179
|
+
const condShape = cond.length === 0 || cond[0] === '[' || cond[0] === '[[' || cond[0] === 'test' || cond[0] === 'true' || cond[0] === ':' ? null : shapeOfWords(cond)
|
|
180
|
+
return `(${label}) ${condShape ?? inner ?? '…'}`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function shapeOfWords(input: string[]): string {
|
|
184
|
+
let words = input
|
|
185
|
+
// unwrap `nice -n 15 timeout 60 env X=1 <cmd>`
|
|
186
|
+
for (;;) {
|
|
187
|
+
const head = words[0]
|
|
188
|
+
if (head == null || !(head in WRAPPERS)) break
|
|
189
|
+
let i = 1
|
|
190
|
+
const eats = ARG_FLAGS[head]
|
|
191
|
+
while (i < words.length && (words[i] as string).startsWith('-')) i += eats?.has(words[i] as string) ? 2 : 1
|
|
192
|
+
i += WRAPPERS[head] as number
|
|
193
|
+
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i] as string)) i++
|
|
194
|
+
if (i >= words.length) break
|
|
195
|
+
words = words.slice(i)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (words.length === 0) return '(empty)'
|
|
199
|
+
if (isScript(words[0] as string)) return base(words[0] as string)
|
|
200
|
+
let program = base(words[0] as string)
|
|
201
|
+
let rest = words.slice(1)
|
|
202
|
+
if ((program === 'bunx' || program === 'npx' || program === 'pnpx') && rest.length > 0) {
|
|
203
|
+
const pkg = rest.findIndex((w) => !w.startsWith('-'))
|
|
204
|
+
if (pkg !== -1) {
|
|
205
|
+
program = `${program} ${base(rest[pkg] as string)}`
|
|
206
|
+
rest = rest.slice(pkg + 1)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (program.startsWith('python')) {
|
|
210
|
+
if (rest[0] === '-c' || rest[0] === '-m') return `${program} ${rest[0]}${rest[0] === '-m' && rest[1] != null ? ` ${rest[1]}` : ''}`
|
|
211
|
+
return rest.some((w) => isPathLike(w)) ? `${program} <file>` : program
|
|
212
|
+
}
|
|
213
|
+
if ((program === 'sed' || program === 'perl') && rest.some((w) => /^-[a-zA-Z]*i/.test(w) || w === '--in-place')) return `${program} -i (edit)`
|
|
214
|
+
if (program === 'cat' && rest[0] === '>') return 'cat > (write)'
|
|
215
|
+
if (NO_SUBCOMMAND.has(program)) return program
|
|
216
|
+
|
|
217
|
+
const parts = [program]
|
|
218
|
+
const eats = ARG_FLAGS[program]
|
|
219
|
+
const depth = TWO_LEVELS.has(program) || program === 'bun' || program === 'npm' || program === 'pnpm' ? 2 : 1
|
|
220
|
+
let subs = 0
|
|
221
|
+
for (let i = 0; i < rest.length; i++) {
|
|
222
|
+
const w = rest[i] as string
|
|
223
|
+
if (w.startsWith('-')) {
|
|
224
|
+
if (eats?.has(w)) i++
|
|
225
|
+
if (subs > 0) break
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
if (isWord(w) && subs < depth) {
|
|
229
|
+
parts.push(w)
|
|
230
|
+
subs++
|
|
231
|
+
// `bun test x`: one level, then the argument is what it runs.
|
|
232
|
+
// `bun run x y` and `bun x y` (a package script, often the
|
|
233
|
+
// project CLI) keep the script and its first word
|
|
234
|
+
if (program === 'bun' && subs === 1 && BUN_BUILTINS.has(w) && w !== 'run') break
|
|
235
|
+
if (program === 'bun' && subs === 2 && parts[1] === 'run' && i + 1 < rest.length && isWord(rest[i + 1] as string)) {
|
|
236
|
+
parts.push(rest[i + 1] as string)
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
if ((program === 'npm' || program === 'pnpm') && subs === 1 && w !== 'run') break
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
if (isPathLike(w) && subs === 0) parts.push('<file>')
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
if ((program === 'sk' || program === 'shipkit') && parts[1] === 'ci') {
|
|
246
|
+
const only = onlyValues(rest)
|
|
247
|
+
if (only.length > 0) return `${program} ci --only ${only.join(',')}`
|
|
248
|
+
}
|
|
249
|
+
if (program === 'bun' && parts[1] === 'test' && parts.length === 2 && rest.some((w) => isPathLike(w))) parts.push('<file>')
|
|
250
|
+
return parts.join(' ')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** the `--only` values of an `sk ci` line, sorted: `--only a --only b,c` */
|
|
254
|
+
function onlyValues(words: string[]): string[] {
|
|
255
|
+
const out: string[] = []
|
|
256
|
+
for (let i = 0; i < words.length; i++) {
|
|
257
|
+
const w = words[i] as string
|
|
258
|
+
if (w === '--only' && words[i + 1] != null) out.push(...(words[++i] as string).split(','))
|
|
259
|
+
else if (w.startsWith('--only=')) out.push(...w.slice(7).split(','))
|
|
260
|
+
}
|
|
261
|
+
return [...new Set(out.filter((x) => x !== ''))].sort()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** the shape of any tool call: Bash by its command, a sub-agent by its type,
|
|
265
|
+
* a skill by its name, everything else by the tool name */
|
|
266
|
+
export function toolShape(name: string, detail: string): string {
|
|
267
|
+
if (name === 'Bash') return commandShape(detail)
|
|
268
|
+
if (name === 'Agent' || name === 'Task') return `Agent ${detail.split(':')[0] || 'general-purpose'}`
|
|
269
|
+
if (name === 'Skill' && detail !== '') return `Skill ${detail}`
|
|
270
|
+
return name
|
|
271
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// What his typed prompts say about the work, by rule, no model: a status ping
|
|
2
|
+
// (he is waiting and asks where things stand), a slowness complaint, a
|
|
3
|
+
// correction. The rules are this visible table, so every count is explainable
|
|
4
|
+
// by the row that matched.
|
|
5
|
+
//
|
|
6
|
+
// The constraint a reader breaks first: his prompts are also SPECS, and a spec
|
|
7
|
+
// says "slow" about the game ("the camera pans too slow"). A slowness rule
|
|
8
|
+
// names the agent's world (tests, the gate, a tool, "takes forever"), never
|
|
9
|
+
// the bare word.
|
|
10
|
+
|
|
11
|
+
export type Signal = 'status' | 'slow' | 'correction'
|
|
12
|
+
|
|
13
|
+
export type Rule = { signal: Signal; id: string; re: RegExp }
|
|
14
|
+
|
|
15
|
+
const AGENT_WORLD = String.raw`(?:tests?|ci|gate|tools?|builds?|agents?|commands?|machine|cpu|checks?|deploys?|hub|servers?|sessions?|lanes?|you|sk|shipkit|typecheck|tsc|lint|everything|things|projects?|progress|it all|shots?|renders?|indexing)`
|
|
16
|
+
|
|
17
|
+
export const RULES: Rule[] = [
|
|
18
|
+
{ signal: 'status', id: 'where-stand', re: /\bwhere (?:do|are) we (?:stand|at)\b/i },
|
|
19
|
+
{ signal: 'status', id: 'progress', re: /\b(?:good progress|any progress|how(?:'s| is) it going|any news|status update|still (?:running|working|on it)|are you (?:done|finished|still))\b/i },
|
|
20
|
+
{ signal: 'status', id: 'done-q', re: /^\s*(?:done|finished|status|progress|eta|so)\s*\?+\s*$/i },
|
|
21
|
+
{ signal: 'status', id: 'fr-avance', re: /\b(?:ça avance|ca avance|où en (?:est|sommes)|ou en est)\b/i },
|
|
22
|
+
|
|
23
|
+
{ signal: 'slow', id: 'too-slow', re: new RegExp(String.raw`\b${AGENT_WORLD}\b[^.?!\n]{0,40}\b(?:too|so|very|really|super|crazy|insanely|extremely|painfully)\s+slow|\b(?:too|so|very|really|super|crazy|insanely|extremely|painfully)\s+slow\b[^.?!\n]{0,30}\b${AGENT_WORLD}\b`, 'i') },
|
|
24
|
+
{ signal: 'slow', id: 'takes-long', re: /\b(?:takes?|taking|took)\s+(?:so\s+|too\s+|way\s+too\s+|really\s+)?(?:long|forever|ages|minutes)\b/i },
|
|
25
|
+
{ signal: 'slow', id: 'world-slow', re: new RegExp(String.raw`\b${AGENT_WORLD}\b[^.?!\n]{0,40}\b(?:slow|sluggish|hangs?|stuck)\b`, 'i') },
|
|
26
|
+
{ signal: 'slow', id: 'slow-world', re: new RegExp(String.raw`\b(?:slow|sluggish)\b[^.?!\n]{0,20}\b${AGENT_WORLD}\b`, 'i') },
|
|
27
|
+
{ signal: 'slow', id: 'make-faster', re: new RegExp(String.raw`\b(?:make|making|get)\s+${AGENT_WORLD}\s+(?:\w+\s+)?faster\b`, 'i') },
|
|
28
|
+
{ signal: 'slow', id: 'still-waiting', re: /\b(?:(?:i'?m|i am|we'?re|we are) still waiting|waiting for ages|been waiting|wait(?:ing)? forever|my cpu|save my machine)\b/i },
|
|
29
|
+
{ signal: 'slow', id: 'slow-equals', re: /\bslow\s*=\s*wrong\b/i },
|
|
30
|
+
|
|
31
|
+
{ signal: 'correction', id: 'double-bang', re: /!!/ },
|
|
32
|
+
{ signal: 'correction', id: 'already-said', re: /\b(?:i (?:already|just) (?:said|told|asked)|how many times|as i said|i said (?:it|that) before|again\s*\?!|stop doing)\b/i },
|
|
33
|
+
{ signal: 'correction', id: 'swear', re: /\b(?:wtf|fuck\w*|ffs|ridiculous|seriously\s*\?|are you kidding)\b/i },
|
|
34
|
+
{ signal: 'correction', id: 'not-what', re: /\b(?:that'?s not what i (?:asked|said|meant)|you (?:didn'?t|did not) (?:read|listen)|you misread|wrong again)\b/i },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/** short words in capitals that are names, never shouting */
|
|
38
|
+
const CAPS_OK = new Set([
|
|
39
|
+
'API', 'URL', 'URLS', 'JSON', 'HTML', 'CSS', 'PR', 'OK', 'TODO', 'README', 'CLI', 'TUI', 'DSL', 'TLDR', 'ASAP', 'GPU', 'CPU', 'IDS',
|
|
40
|
+
'FAIL', 'WARN', 'GO', 'UI', 'UX', 'SVG', 'PNG', 'MP4', 'SQL', 'HTTP', 'HTTPS', 'SSH', 'NPM', 'TS', 'JS', 'MD', 'IMO', 'FYI', 'LLM',
|
|
41
|
+
'AGENTS', 'CLAUDE', 'STE', 'VAT', 'TVA', 'OVH', 'DNS', 'CDN', 'OG', 'RGB', 'FPS', 'HMR', 'MCP', 'SDK', 'AI', 'GIF', 'PDF', 'EN', 'FR',
|
|
42
|
+
'WIP', 'MVP', 'VS', 'ID', 'NPC', 'NPCS', 'HUD', 'HP', 'XP', 'UTC', 'CET', 'ENV', 'EOF', 'RAM', 'SSD', 'USB', 'AND', 'OR', 'NOT',
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
/** code and pastes say nothing about his mood: fenced blocks, inline code and
|
|
46
|
+
* quoted pastes are dropped before any rule runs */
|
|
47
|
+
export function prose(text: string): string {
|
|
48
|
+
return text
|
|
49
|
+
.replace(/<pasted_content\b[\s\S]*?(?:<\/pasted_content>|$)/g, ' ')
|
|
50
|
+
.replace(/```[\s\S]*?(?:```|$)/g, ' ')
|
|
51
|
+
.replace(/`[^`\n]*`/g, ' ')
|
|
52
|
+
.split('\n')
|
|
53
|
+
// quoted lines, tables, stack frames, shell lines, and an agent's report
|
|
54
|
+
// pasted back (its lines open on a skim marker)
|
|
55
|
+
.filter((l) => !/^\s*(?:>|\||\d+\s*[|:]|at |\$ |[✅💬🔴🔶🧹💡❓🤝🚧🔼🚀🔒💥💸])/u.test(l))
|
|
56
|
+
.join('\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** two or more shouted words (three letters or more, not a known name) */
|
|
60
|
+
export function shouts(text: string): number {
|
|
61
|
+
const words = text.match(/\b[A-Z][A-Z']{2,}\b/g) ?? []
|
|
62
|
+
return words.filter((w) => !CAPS_OK.has(w.replace(/'.*$/, '')) && !/^[A-Z]{2,}\d/.test(w)).length
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type SignalHit = { signal: Signal; rule: string }
|
|
66
|
+
|
|
67
|
+
/** every signal one prompt carries, with the rule that matched each */
|
|
68
|
+
/** joy shouts too: `!!` and capitals next to these words are delight, not a correction */
|
|
69
|
+
const JOY = /\b(?:love|amazing|great|awesome|perfect|fantastic|wonderful|beautiful|wow|nice|yes|excellent|brilliant|bravo|incredible|gorgeous)\b/i
|
|
70
|
+
|
|
71
|
+
export function signalsOf(text: string): SignalHit[] {
|
|
72
|
+
const body = prose(text).slice(0, 2000)
|
|
73
|
+
const hits: SignalHit[] = []
|
|
74
|
+
const seen = new Set<Signal>()
|
|
75
|
+
const joy = JOY.test(body)
|
|
76
|
+
for (const r of RULES) {
|
|
77
|
+
if (joy && r.id === 'double-bang') continue
|
|
78
|
+
if (seen.has(r.signal) || !r.re.test(body)) continue
|
|
79
|
+
seen.add(r.signal)
|
|
80
|
+
hits.push({ signal: r.signal, rule: r.id })
|
|
81
|
+
}
|
|
82
|
+
if (!seen.has('correction') && !joy && shouts(body) >= 2) hits.push({ signal: 'correction', rule: 'caps' })
|
|
83
|
+
return hits
|
|
84
|
+
}
|