Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
59e2a9d01This file is pinned to the selected release. Download the original to open it in your design tool.
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { PROCESS_ENGINES } from '../engines/types.js'
import type { AgentCommandOwnershipSnapshot } from './engineBin.js'
import {
ambiguousAgentProcess,
bypassPermissionActive,
permissionModeFromArgv,
engineProcessMatch,
engineProcessMatchScore,
LSTART_MARKER_RE,
parseProcessRow,
resumeSessionId,
sendLiteralToTmux,
sendToTmux,
tmuxCaptureArgs,
} from './tmux.js'
const ownership = (cursor: string[] = [], grok: string[] = []): AgentCommandOwnershipSnapshot => ({
cursorFileKeys: new Set(cursor),
grokFileKeys: new Set(grok),
conflictingFileKeys: new Set(cursor.filter((key) => grok.includes(key))),
agentCandidates: [],
cursorAgentCandidates: [],
grokCandidates: [],
})
describe('tmux process primitives', () => {
it('parses a process whose comm field contains spaces', () => {
expect(parseProcessRow('4242 100 ⌘ Greeting Thu Jul 30 11:00:03 2026 cmd -r abcdef12-3456-7890-abcd-ef1234567890')).toEqual({
pid: 4242,
parentPid: 100,
executable: '⌘ Greeting',
startMarker: 'Thu Jul 30 11:00:03 2026',
args: 'cmd -r abcdef12-3456-7890-abcd-ef1234567890',
})
})
/**
* Real `ps -axo pid=,ppid=,comm=,lstart=,args=` output captured on Ubuntu 24.04 (procps-ng 4.0.4),
* not a macOS-shaped invention. Two things differ from macOS and both are load-bearing:
* - `comm` is a bare name, never a path (`/sbin/docker-init` reads back as `docker-init`);
* - `comm` is hard-capped at 15 bytes by TASK_COMM_LEN, so the binary really named
* `a-very-long-engine-name-binary` reads back as `a-very-long-eng`.
* `tmux: server` also proves comm can still carry a space on Linux, which is why the parser anchors
* on lstart rather than splitting comm as one token.
*/
it('parses real Linux procps rows, including a 15-byte-truncated comm', () => {
expect(parseProcessRow(' 1 0 docker-init Fri Aug 21 09:28:14 2026 /sbin/docker-init -- bash -lc')).toEqual({
pid: 1,
parentPid: 0,
executable: 'docker-init',
startMarker: 'Fri Aug 21 09:28:14 2026',
args: '/sbin/docker-init -- bash -lc',
})
expect(parseProcessRow(' 15 1 tmux: server Fri Aug 21 09:28:14 2026 tmux new-session -d -s probe sleep 400')).toEqual({
pid: 15,
parentPid: 1,
executable: 'tmux: server',
startMarker: 'Fri Aug 21 09:28:14 2026',
args: 'tmux new-session -d -s probe sleep 400',
})
const truncated = parseProcessRow(
' 12 7 a-very-long-eng Fri Aug 21 09:28:14 2026 /tmp/a-very-long-engine-name-binary -e x')
expect(truncated?.executable).toBe('a-very-long-eng')
expect(Buffer.byteLength(truncated!.executable)).toBe(15)
})
/**
* Linux procps substitutes `?` for any byte it cannot print in the current locale, and a daemon under
* systemd/docker/ssh usually has no locale at all. Measured on Ubuntu 24.04: a Command Code pane reads
* back as `??? <title>` in BOTH comm and args, so both halves of its marker fail and the reaper evicts
* the live pane. processRows() now reads ps under LC_ALL=C.UTF-8 and repairs any surviving `?` row from
* /proc, which is raw bytes. These two cases pin the before and the after.
*/
/**
* `lstart`'s shape belongs to LC_TIME, not to `ps`. Captured with
* `LC_TIME=<locale> ps -axo pid=,ppid=,comm=,lstart=,args=` on macOS 15.5, one run per locale on the
* same machine in the same second. Of sixteen locales tried only C, en_US and hu_HU parse at all; the
* rest reorder the fields and yield ZERO rows out of ~590.
*
* Zero rows is the whole bug: processRows returns `[]`, not null, so "we looked and the machine is
* empty" is indistinguishable from the truth, resolvePaneEngineProcess finds no engine under any pane,
* and watchCreatedPane burns its full ten minutes before reporting START_TIMEOUT — "claude did not
* expose an engine process within 10 minutes" — against a pane where claude is running and still
* firing hooks. This is why processRows spawns ps under psEnv instead of inheriting the locale.
*/
it('cannot parse an lstart column written by a locale that reorders it', () => {
const rows = [
'15160 2281 /Users/admin/.lo Tue 15 Sep 23:10:38 2026 /Users/admin/.local/bin/claude',
'15160 2281 /Users/admin/.lo Di. 15 Sep. 23:10:38 2026 /Users/admin/.local/bin/claude',
'15160 2281 /Users/admin/.lo mar. 15 sept. 23:10:38 2026 /Users/admin/.local/bin/claude',
'15160 2281 /Users/admin/.lo \u706b 9/15 23:10:38 2026 /Users/admin/.local/bin/claude',
'15160 2281 /Users/admin/.lo \u0432\u0442\u043e\u0440\u043d\u0438\u043a, 15 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f 2026 \u0433. 23:10:38 /Users/admin/.local/bin/claude',
]
for (const row of rows) expect(parseProcessRow(row)).toBeNull()
// The same process, same second, under the LC_TIME=C that psEnv guarantees.
expect(parseProcessRow('15160 2281 /Users/admin/.lo Tue Sep 15 23:10:38 2026 /Users/admin/.local/bin/claude'))
.toEqual({
pid: 15160,
parentPid: 2281,
executable: '/Users/admin/.lo',
startMarker: 'Tue Sep 15 23:10:38 2026',
args: '/Users/admin/.local/bin/claude',
})
})
/**
* checkSessionRuntime adopts, rather than compares, any saved marker this rejects. It has to reject
* localized stamps too: a marker recorded before psEnv landed (hu_HU parsed fine, `K szept. 15 …`) can
* never equal the C-locale stamp read back for the same live process, and comparing them would evict a
* live pane exactly once per session on upgrade.
*/
it('recognises only a C-locale lstart stamp as a comparable start marker', () => {
expect(LSTART_MARKER_RE.test('Tue Sep 15 23:10:38 2026')).toBe(true)
expect(LSTART_MARKER_RE.test('Fri Aug 21 09:28:14 2026')).toBe(true)
expect(LSTART_MARKER_RE.test('K szept. 15 23:10:38 2026')).toBe(false) // hu_HU, parsed pre-fix
expect(LSTART_MARKER_RE.test('Tue 15 Sep 23:10:38 2026')).toBe(false) // en_AU
expect(LSTART_MARKER_RE.test('Greeting Thu Jul 30 11:00:03 2026')).toBe(false) // pre-fix shifted comm
})
it('scores Command Code from the real bytes, and cannot from the mangled ones', () => {
const mangled = parseProcessRow(' 185 178 ??? harness-cli Fri Aug 21 09:34:20 2026 ??? harness-cli ubuntu probe')
expect(engineProcessMatchScore(mangled!, 'commandcode')).toBe(0)
const repaired = parseProcessRow(' 185 178 ⌘ harness-cli Fri Aug 21 09:34:20 2026 ⌘ harness-cli ubuntu probe')
expect(engineProcessMatchScore(repaired!, 'commandcode')).toBe(3)
})
it('recognises stable installed binary forms', () => {
expect(engineProcessMatchScore({ executable: 'devin', args: 'devin' }, 'devin')).toBe(3)
expect(engineProcessMatchScore({ executable: 'muse-bin-0.1.0-R708.1', args: 'muse-bin-0.1.0-R708.1' }, 'muse')).toBe(3)
expect(engineProcessMatchScore({ executable: '/Users/demo/.grok/bin/grok', args: 'grok' }, 'grok')).toBe(3)
})
it.each([
['codex', 'codex-aarch64-apple-darwin'],
['codex', 'codex-x86_64-unknown-linux-musl'],
['kilo', 'kilo-darwin-arm64'],
['kilo', 'kilo-linux-x64-baseline'],
['grok', 'grok-1.0.5-macos-aarch64'],
['grok', 'grok-linux-x86_64'],
] as const)('recognises the %s standalone release image %s', (engine, executable) => {
expect(engineProcessMatchScore({ executable, args: executable }, engine)).toBe(3)
})
it('uses installed executable identity ahead of misleading argv names', () => {
const allFiles = new Map([['codex', new Set(['codex-native'])]]) as AgentCommandOwnershipSnapshot['engineFileKeys']
const commands: AgentCommandOwnershipSnapshot = {
...ownership(),
engineFileKeys: allFiles,
engineCandidates: new Map(),
}
const row = {
executable: 'renamed-release-image',
args: 'renamed-release-image --some-flag',
imagePath: '/custom/native/renamed-release-image',
imageFileKey: 'codex-native',
}
expect(engineProcessMatch(row, 'codex', commands)).toEqual({
score: 4,
evidence: 'file-identity',
imagePath: '/custom/native/renamed-release-image',
})
expect(engineProcessMatchScore(row, 'claude', commands)).toBe(0)
})
it.each(PROCESS_ENGINES)('recognises a renamed native %s image from installed file identity', (engine) => {
const key = `native-${engine}`
const commands: AgentCommandOwnershipSnapshot = {
...ownership(),
engineFileKeys: new Map([[engine, new Set([key])]]),
engineCandidates: new Map(),
}
expect(engineProcessMatch({
executable: 'vendor-rewritten-title',
args: 'vendor-rewritten-title',
imageFileKey: key,
}, engine, commands)).toMatchObject({ score: 4, evidence: 'file-identity' })
})
it('rejects Antigravity IDE binaries even when their basename is agy', () => {
expect(engineProcessMatchScore({
executable: '/Applications/Antigravity.app/Contents/Resources/bin/agy',
args: '/Applications/Antigravity.app/Contents/Resources/bin/agy',
}, 'agy')).toBe(0)
})
it('recognises Claude native installer version targets without accepting a bare semver', () => {
expect(engineProcessMatchScore({
executable: '/Users/demo/.local/share/claude/versions/2.1.246',
args: '/Users/demo/.local/share/claude/versions/2.1.246',
}, 'claude')).toBe(3)
expect(engineProcessMatchScore({
executable: '2.1.246',
args: '/home/demo/.local/share/claude/versions/2.1.246',
}, 'claude')).toBe(3)
expect(engineProcessMatchScore({ executable: '2.1.246', args: '2.1.246' }, 'claude')).toBe(0)
})
it('reads an engine through the ori launcher, before and after its exec', () => {
// `ori claude` computes an environment and then execve's the vendor binary away, so for all but the
// first ~100ms the pane row IS `claude` — that case must keep scoring exactly as a bare launch does.
expect(engineProcessMatchScore({ executable: 'claude', args: '/Users/demo/.local/bin/claude --resume x' }, 'claude')).toBe(3)
// The pre-exec window (and any future ori that spawns instead of exec'ing) resolves through the flags.
expect(engineProcessMatchScore({ executable: 'ori', args: 'ori claude' }, 'claude')).toBe(3)
expect(engineProcessMatchScore({ executable: 'ori', args: '/Users/demo/.local/bin/ori claude --model anthropic/claude-sonnet-4.6 -p hi' }, 'claude')).toBe(3)
expect(engineProcessMatchScore({ executable: 'ori', args: 'ori --log-level debug codex --full-auto' }, 'codex')).toBe(3)
expect(engineProcessMatchScore({ executable: 'ori', args: 'ori opencode' }, 'opencode')).toBe(3)
// Wrapping does not make it a different engine, and ori's own subcommands are not engines.
expect(engineProcessMatchScore({ executable: 'ori', args: 'ori claude' }, 'codex')).toBe(0)
expect(engineProcessMatchScore({ executable: 'ori', args: 'ori eval' }, 'claude')).toBe(0)
expect(engineProcessMatchScore({ executable: 'ori', args: 'ori login' }, 'claude')).toBe(0)
})
it('assigns the colliding agent basename only from executable ownership', () => {
const cursor = ownership(['cursor-file'], ['grok-file'])
const grok = ownership(['cursor-file'], ['grok-file'])
const cursorRow = { executable: 'agent', args: 'agent', imageFileKey: 'cursor-file' }
const grokRow = { executable: 'agent', args: 'agent', imageFileKey: 'grok-file' }
expect(engineProcessMatchScore(cursorRow, 'cursor', cursor)).toBe(4)
expect(engineProcessMatchScore(cursorRow, 'grok', cursor)).toBe(0)
expect(engineProcessMatchScore(grokRow, 'grok', grok)).toBe(4)
expect(engineProcessMatchScore(grokRow, 'cursor', grok)).toBe(0)
expect(engineProcessMatchScore({ executable: 'agent', args: 'agent' }, 'cursor', cursor)).toBe(0)
const conflict = ownership(['same-file'], ['same-file'])
expect(engineProcessMatchScore({ executable: 'agent', args: 'agent', imageFileKey: 'same-file' }, 'cursor', conflict)).toBe(0)
expect(engineProcessMatchScore({ executable: 'agent', args: 'agent', imageFileKey: 'same-file' }, 'grok', conflict)).toBe(0)
})
it('does not treat a daemon role named agent as the colliding CLI command', () => {
expect(ambiguousAgentProcess({
executable: '/usr/sbin/distnoted',
args: '/usr/sbin/distnoted agent',
}, ownership())).toBe(false)
expect(ambiguousAgentProcess({
executable: '/usr/sbin/cfprefsd',
args: '/usr/sbin/cfprefsd agent',
}, ownership())).toBe(false)
})
it('extracts only explicit resume ids', () => {
expect(resumeSessionId('cursor', 'agent --resume=53d3843c-724e-47ff-ae3a-9fedfa328bba'))
.toBe('53d3843c-724e-47ff-ae3a-9fedfa328bba')
expect(resumeSessionId('opencode', 'opencode -s ses_05e335115ffeM05DT5hJHeN3Vp'))
.toBe('ses_05e335115ffeM05DT5hJHeN3Vp')
expect(resumeSessionId('hermes', 'hermes --resume 20260728_115628_f2c86a'))
.toBe('20260728_115628_f2c86a')
expect(resumeSessionId('kilo', 'kilo --session ses_024a007fdffe11yG68JPxsHJly'))
.toBe('ses_024a007fdffe11yG68JPxsHJly')
expect(resumeSessionId('pi', 'pi --session-id 53d3843c-724e-47ff-ae3a-9fedfa328bba'))
.toBe('53d3843c-724e-47ff-ae3a-9fedfa328bba')
expect(resumeSessionId('commandcode', 'cmd --resume 53d3843c-724e-47ff-ae3a-9fedfa328bba'))
.toBe('53d3843c-724e-47ff-ae3a-9fedfa328bba')
expect(resumeSessionId('muse', 'muse resume 53d3843c-724e-47ff-ae3a-9fedfa328bba'))
.toBe('53d3843c-724e-47ff-ae3a-9fedfa328bba')
expect(resumeSessionId('amp', 'amp threads continue T-019fda49-724e-47ff-ae3a-9fedfa328bba'))
.toBe('T-019fda49-724e-47ff-ae3a-9fedfa328bba')
expect(resumeSessionId('grok', 'grok -r 53d3843c-724e-47ff-ae3a-9fedfa328bba'))
.toBe('53d3843c-724e-47ff-ae3a-9fedfa328bba')
expect(resumeSessionId('commandcode', 'cmd -r Greeting')).toBeNull()
expect(resumeSessionId('devin', 'devin --resume 53d3843c-724e-47ff-ae3a-9fedfa328bba')).toBeNull()
})
it('reads a claude/codex resume id from argv, but never the parent of a fork', () => {
// A daemon that could not see the pane when SessionStart fired (a session named by an older build)
// has only argv to learn the session from — measured on machine-remote-1, where six agents sat
// sessionless for ten days and could not be forked.
expect(resumeSessionId('claude', '/opt/agent-cli/.local/bin/claude --resume f56f0a36-aa58-4af1-a6e2-a77386122332'))
.toBe('f56f0a36-aa58-4af1-a6e2-a77386122332')
expect(resumeSessionId('claude', 'claude --dangerously-skip-permissions -r f4749d75-aef8-4d07-8031-48e2abecf7e5'))
.toBe('f4749d75-aef8-4d07-8031-48e2abecf7e5')
expect(resumeSessionId('codex', 'node /usr/local/bin/codex resume 53d3843c-724e-47ff-ae3a-9fedfa328bba --approve-for-me'))
.toBe('53d3843c-724e-47ff-ae3a-9fedfa328bba')
// `--resume <parent> --fork-session` writes a NEW session: the id on argv is the parent's.
expect(resumeSessionId('claude', 'claude --resume 53d3843c-724e-47ff-ae3a-9fedfa328bba --fork-session')).toBeNull()
// `codex fork <parent>` likewise names the parent, and is not a resume.
expect(resumeSessionId('codex', 'codex fork 53d3843c-724e-47ff-ae3a-9fedfa328bba')).toBeNull()
expect(resumeSessionId('claude', 'claude --continue')).toBeNull()
})
it('reads the exact permission mode a live process was launched with', () => {
expect(permissionModeFromArgv('claude', '/usr/local/bin/claude --dangerously-skip-permissions')).toBe('full')
expect(permissionModeFromArgv('claude', 'claude --permission-mode plan --resume abc')).toBe('plan')
expect(permissionModeFromArgv('claude', 'claude --permission-mode=acceptEdits')).toBe('acceptEdits')
expect(permissionModeFromArgv('claude', 'claude --permission-mode auto')).toBe('auto')
expect(permissionModeFromArgv('codex', 'codex --dangerously-bypass-approvals-and-sandbox')).toBe('full')
expect(permissionModeFromArgv('codex', 'codex --sandbox read-only')).toBe('readOnly')
expect(permissionModeFromArgv('codex', 'codex resume abc --approve-for-me')).toBe('auto')
expect(permissionModeFromArgv('cursor', 'cursor-agent --force')).toBe('auto')
expect(permissionModeFromArgv('opencode', 'opencode --auto')).toBe('auto')
// Skip-everything outranks a mode named beside it: that process runs without permissions.
expect(permissionModeFromArgv('claude', 'claude --permission-mode plan --dangerously-skip-permissions')).toBe('full')
})
it('names no mode for an argv that carries none — never `ask`, never a guess', () => {
// No flag is silence, not a choice; `bypassPermission` keeps deciding there.
expect(permissionModeFromArgv('claude', 'claude --resume abc')).toBeNull()
// A mode the table does not know cannot be reapplied, so it is not recorded.
expect(permissionModeFromArgv('claude', 'claude --permission-mode bogus')).toBeNull()
expect(permissionModeFromArgv('claude', 'claude --permission-mode manual auto')).toBeNull()
expect(permissionModeFromArgv('codex', 'codex --sandbox workspace-write')).toBeNull()
// Only an exact token counts, as for the bypass flag.
expect(permissionModeFromArgv('claude', 'claude "please avoid --dangerously-skip-permissions for now"')).toBeNull()
expect(permissionModeFromArgv('claude', 'claude --dangerously-skip-permissions-explained')).toBeNull()
// Engines with no mode table.
expect(permissionModeFromArgv('pi', 'pi --dangerously-skip-permissions')).toBeNull()
expect(permissionModeFromArgv('terminal', 'zsh -l')).toBeNull()
})
it('reads bypass-permission mode from a live process argv via exact token match', () => {
expect(bypassPermissionActive('claude', '/usr/local/bin/claude --permission-mode auto')).toBe(true)
expect(bypassPermissionActive('claude', 'claude --permission-mode=auto --resume abc')).toBe(true)
expect(bypassPermissionActive('codex', 'codex resume abc --approve-for-me')).toBe(true)
// Launched before the auto modes: the old flags still count as approving (and, recorded as the
// `full` mode, come back as themselves on a relaunch).
expect(bypassPermissionActive('claude', '/usr/local/bin/claude --dangerously-skip-permissions'))
.toBe(true)
expect(bypassPermissionActive('codex', 'codex --dangerously-bypass-approvals-and-sandbox'))
.toBe(true)
expect(bypassPermissionActive('cursor', 'cursor-agent --force')).toBe(true)
expect(bypassPermissionActive('opencode', 'opencode --auto')).toBe(true)
})
it('does not false-positive on a flag that only appears as a substring', () => {
// The flag text can legitimately appear inside a prompt/argument; only an exact token counts.
expect(bypassPermissionActive('claude', 'claude "please avoid --dangerously-skip-permissions for now"'))
.toBe(false)
expect(bypassPermissionActive('claude', 'claude --dangerously-skip-permissions-explained')).toBe(false)
})
it('reads false when the confirmed flag is absent', () => {
expect(bypassPermissionActive('claude', 'claude --resume abc')).toBe(false)
// Another mode, or "auto" that is not the mode's value.
expect(bypassPermissionActive('claude', 'claude --permission-mode plan')).toBe(false)
expect(bypassPermissionActive('codex', 'codex --sandbox read-only')).toBe(false)
expect(bypassPermissionActive('claude', 'claude --permission-mode manual auto')).toBe(false)
expect(bypassPermissionActive('claude', 'claude auto --permission-mode')).toBe(false)
})
it('always reads false for engines with no confirmed bypass flag — never guesses', () => {
expect(bypassPermissionActive('pi', 'pi --dangerously-skip-permissions')).toBe(false)
expect(bypassPermissionActive('hermes', 'hermes --dangerously-skip-permissions')).toBe(false)
expect(bypassPermissionActive('devin', 'devin --dangerously-skip-permissions')).toBe(false)
})
it('maps neutral visible/history and ANSI capture options to tmux flags', () => {
expect(tmuxCaptureArgs('%7', 60)).toEqual([
'capture-pane', '-p', '-e', '-J', '-t', '%7', '-S', '-60',
])
expect(tmuxCaptureArgs('%7', 60, { visible: true, ansi: false })).toEqual([
'capture-pane', '-p', '-J', '-t', '%7',
])
})
it('carries prompt and literal bytes only over stdin, never child argv or diagnostics', async () => {
const dir = mkdtempSync(join(tmpdir(), 'harness-tmux-input-'))
const argsFile = join(dir, 'args')
const stdinFile = join(dir, 'stdin')
const fakeTmux = join(dir, 'tmux')
const sentinel = `prompt sentinel $' ${process.pid}`
writeFileSync(fakeTmux, `#!/bin/sh
printf '%s\\n' "$*" >> "$TMUX_INPUT_ARGS"
if [ "$1" = "load-buffer" ]; then
cat >> "$TMUX_INPUT_STDIN"
printf '\\n' >> "$TMUX_INPUT_STDIN"
fi
if [ "$1" = "paste-buffer" ] && [ "$TMUX_INPUT_FAIL_PASTE" = "1" ]; then
printf 'synthetic paste failure\\n' >&2
exit 2
fi
`)
chmodSync(fakeTmux, 0o700)
const previous = {
path: process.env.PATH,
args: process.env.TMUX_INPUT_ARGS,
stdin: process.env.TMUX_INPUT_STDIN,
fail: process.env.TMUX_INPUT_FAIL_PASTE,
}
const errors: string[] = []
const error = vi.spyOn(console, 'error').mockImplementation((...values) => {
errors.push(values.map(String).join(' '))
})
try {
process.env.PATH = `${dir}:${previous.path ?? ''}`
process.env.TMUX_INPUT_ARGS = argsFile
process.env.TMUX_INPUT_STDIN = stdinFile
expect(await sendLiteralToTmux('%7', sentinel)).toBe(true)
expect(await sendToTmux('%7', sentinel)).toBe(true)
process.env.TMUX_INPUT_FAIL_PASTE = '1'
expect(await sendLiteralToTmux('%7', sentinel)).toBe(false)
const argv = readFileSync(argsFile, 'utf8')
expect(argv).not.toContain(sentinel)
expect(readFileSync(stdinFile, 'utf8').split(sentinel)).toHaveLength(4)
expect(errors.join('\n')).not.toContain(sentinel)
} finally {
error.mockRestore()
if (previous.path === undefined) delete process.env.PATH
else process.env.PATH = previous.path
if (previous.args === undefined) delete process.env.TMUX_INPUT_ARGS
else process.env.TMUX_INPUT_ARGS = previous.args
if (previous.stdin === undefined) delete process.env.TMUX_INPUT_STDIN
else process.env.TMUX_INPUT_STDIN = previous.stdin
if (previous.fail === undefined) delete process.env.TMUX_INPUT_FAIL_PASTE
else process.env.TMUX_INPUT_FAIL_PASTE = previous.fail
rmSync(dir, { recursive: true, force: true })
}
})
})