opencode-onboard 0.4.3 → 0.4.5

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.
Files changed (36) hide show
  1. package/README.md +41 -40
  2. package/content/.agents/agents/devops-manager.md +123 -123
  3. package/content/.agents/skills/ob-default/SKILL.md +25 -21
  4. package/content/.agents/skills/ob-generic-guardrails/SKILL.md +36 -32
  5. package/content/.agents/skills/ob-global/SKILL.md +92 -84
  6. package/content/.agents/skills/ob-pullrequest-az/SKILL.md +168 -160
  7. package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +140 -136
  8. package/content/.opencode/commands/create-engineer.md +109 -0
  9. package/content/.opencode/plugins/session-log.js +523 -519
  10. package/content/AGENTS.md +32 -21
  11. package/package.json +1 -1
  12. package/src/commands/wizard.js +124 -113
  13. package/src/presets/browser.json +22 -18
  14. package/src/presets/optimization.json +27 -22
  15. package/src/steps/browser/browser.test.js +115 -81
  16. package/src/steps/browser/index.js +62 -54
  17. package/src/steps/clean/index.js +108 -107
  18. package/src/steps/metadata/index.js +63 -62
  19. package/src/steps/models/format.js +61 -60
  20. package/src/steps/models/write.test.js +117 -117
  21. package/src/steps/openspec/ensemble.test.js +79 -79
  22. package/src/steps/openspec/index.js +121 -32
  23. package/src/steps/openspec/index.test.js +63 -0
  24. package/src/steps/optimization/caveman.js +34 -29
  25. package/src/steps/optimization/codegraph.js +103 -0
  26. package/src/steps/optimization/codegraph.test.js +104 -0
  27. package/src/steps/optimization/global.js +88 -64
  28. package/src/steps/optimization/global.test.js +99 -0
  29. package/src/steps/optimization/index.js +109 -101
  30. package/src/steps/optimization/optimization.test.js +101 -93
  31. package/src/steps/optimization/quota.js +84 -84
  32. package/src/steps/source/source.test.js +124 -124
  33. package/src/utils/__tests__/copy.test.js +117 -117
  34. package/src/utils/exec-spinner.js +47 -47
  35. package/src/utils/exec.js +134 -131
  36. package/src/utils/terminal.js +6 -0
@@ -1,54 +1,62 @@
1
- import { execa } from 'execa'
2
- import fse from 'fs-extra'
3
- import { header, info, success, warn, error } from '../../utils/exec.js'
4
- import os from 'os'
5
- import path from 'path'
6
- import { fileURLToPath } from 'url'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
- const BROWSER_PRESET_PATH = path.resolve(__dirname, '../../presets/browser.json')
10
- const browserPreset = await fse.readJson(BROWSER_PRESET_PATH)
11
-
12
- export async function installBrowser() {
13
- header('Step 9, Installing opencode-browser')
14
-
15
- try {
16
- const child = execa(browserPreset.installer.command, browserPreset.installer.args, {
17
- cwd: os.homedir(),
18
- stdio: ['pipe', 'pipe', 'pipe'],
19
- reject: false,
20
- })
21
-
22
- const pendingTriggers = [...browserPreset.autoAnswers]
23
- let show = false
24
-
25
- child.stdout.on('data', (chunk) => {
26
- const text = chunk.toString()
27
-
28
- if (text.includes(browserPreset.output.showAfter)) show = true
29
- if (text.includes(browserPreset.output.hideAfter)) show = false
30
-
31
- if (show) process.stdout.write(chunk)
32
-
33
- for (let i = 0; i < pendingTriggers.length; i++) {
34
- if (text.includes(pendingTriggers[i].trigger)) {
35
- child.stdin.write(pendingTriggers[i].response + '\n')
36
- pendingTriggers.splice(i, 1)
37
- break
38
- }
39
- }
40
- })
41
-
42
- child.stderr.on('data', (chunk) => process.stderr.write(chunk))
43
-
44
- const result = await child
45
-
46
- if (result.exitCode === 0) {
47
- success('opencode-browser installed')
48
- } else {
49
- warn('opencode-browser install exited with non-zero code')
50
- }
51
- } catch (err) {
52
- error(`Failed to install opencode-browser: ${err.message}`)
53
- }
54
- }
1
+ import { execa } from 'execa'
2
+ import fse from 'fs-extra'
3
+ import { header, success, warn, error } from '../../utils/exec.js'
4
+ import os from 'os'
5
+ import path from 'path'
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const BROWSER_PRESET_PATH = path.resolve(__dirname, '../../presets/browser.json')
10
+ const browserPreset = await fse.readJson(BROWSER_PRESET_PATH)
11
+
12
+ export async function installBrowser(ctx = {}) {
13
+ header('Step 9, Installing opencode-browser')
14
+
15
+ const installScope = ctx.installScope || 'local'
16
+ const locationAnswer = browserPreset.locationChoices?.[installScope] ?? browserPreset.locationChoices?.local ?? '2'
17
+
18
+ const pendingTriggers = browserPreset.autoAnswers.map(a => ({
19
+ ...a,
20
+ response: a.response === '__LOCATION__' ? locationAnswer : a.response,
21
+ }))
22
+
23
+ try {
24
+ const child = execa(browserPreset.installer.command, browserPreset.installer.args, {
25
+ cwd: os.homedir(),
26
+ stdio: ['pipe', 'pipe', 'pipe'],
27
+ reject: false,
28
+ })
29
+
30
+ let show = false
31
+ const triggers = [...pendingTriggers]
32
+
33
+ child.stdout.on('data', (chunk) => {
34
+ const text = chunk.toString()
35
+
36
+ if (text.includes(browserPreset.output.showAfter)) show = true
37
+ if (text.includes(browserPreset.output.hideAfter)) show = false
38
+
39
+ if (show) process.stdout.write(chunk)
40
+
41
+ for (let i = 0; i < triggers.length; i++) {
42
+ if (text.includes(triggers[i].trigger)) {
43
+ child.stdin.write(`${triggers[i].response}\n`)
44
+ triggers.splice(i, 1)
45
+ break
46
+ }
47
+ }
48
+ })
49
+
50
+ child.stderr.on('data', (chunk) => process.stderr.write(chunk))
51
+
52
+ const result = await child
53
+
54
+ if (result.exitCode === 0) {
55
+ success('opencode-browser installed')
56
+ } else {
57
+ warn('opencode-browser install exited with non-zero code')
58
+ }
59
+ } catch (err) {
60
+ error(`Failed to install opencode-browser: ${err.message}`)
61
+ }
62
+ }
@@ -1,107 +1,108 @@
1
- import { checkbox } from '@inquirer/prompts'
2
- import fse from 'fs-extra'
3
- import path from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { findAiFiles } from '../../utils/copy.js'
6
- import { header, info, success, warn } from '../../utils/exec.js'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
- const CLEAN_PRESET_PATH = path.resolve(__dirname, '../../presets/clean.json')
10
- const cleanPreset = await fse.readJson(CLEAN_PRESET_PATH)
11
-
12
- async function childrenExcludingPreserved(dir) {
13
- const results = []
14
- if (!await fse.pathExists(dir)) return results
15
- const entries = await fse.readdir(dir)
16
- for (const entry of entries) {
17
- if (cleanPreset.preserveSubfolders.includes(entry)) continue
18
- results.push(path.join(dir, entry))
19
- }
20
- return results
21
- }
22
-
23
- async function isPopulated(filePath) {
24
- if (!await fse.pathExists(filePath)) return false
25
- const content = await fse.readFile(filePath, 'utf-8')
26
- const trimmed = content.trim()
27
- if (!trimmed) return false
28
- if (trimmed.startsWith('<!-- onboard-prompt')) return false
29
- return true
30
- }
31
-
32
- async function hasOpenspecHistory(cwd) {
33
- const changesDir = path.join(cwd, 'openspec', 'changes')
34
- const archiveDir = path.join(cwd, 'openspec', 'archive')
35
- if (await fse.pathExists(changesDir)) {
36
- const entries = await fse.readdir(changesDir)
37
- if (entries.length > 0) return true
38
- }
39
- if (await fse.pathExists(archiveDir)) {
40
- const entries = await fse.readdir(archiveDir)
41
- if (entries.length > 0) return true
42
- }
43
- return false
44
- }
45
-
46
- export async function cleanAiFiles() {
47
- header('Step 2, Existing AI config files')
48
-
49
- const cwd = process.cwd()
50
- const ctx = {
51
- hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
52
- hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
53
- hasOpenspec: await hasOpenspecHistory(cwd),
54
- }
55
-
56
- if (ctx.hasDesign) info('DESIGN.md exists and is populated, keeping it')
57
- if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
58
- if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
59
-
60
- const flatFiles = await findAiFiles(cwd, cleanPreset.detectFiles)
61
- const dirTargets = cleanPreset.directoryTargets
62
- const dirEntries = []
63
- for (const dirName of dirTargets) {
64
- const dirPath = path.join(cwd, dirName)
65
- const children = await childrenExcludingPreserved(dirPath)
66
- dirEntries.push(...children)
67
- }
68
-
69
- const filteredFlat = flatFiles.filter(f => {
70
- const rel = path.relative(cwd, f)
71
- if (dirTargets.includes(rel)) return false
72
- if (cleanPreset.preserve.some(p => rel === p || rel.startsWith(p + path.sep))) return false
73
- return true
74
- })
75
-
76
- const allToRemove = [...filteredFlat, ...dirEntries]
77
-
78
- if (allToRemove.length === 0) {
79
- success('No existing AI config files to remove')
80
- return ctx
81
- }
82
-
83
- const choices = allToRemove.map(f => ({
84
- name: path.relative(cwd, f).replace(/\\/g, '/'),
85
- value: f,
86
- checked: true,
87
- }))
88
-
89
- const selected = await checkbox({
90
- message: cleanPreset.selectionMessage,
91
- choices,
92
- })
93
-
94
- if (!selected || selected.length === 0) {
95
- success('No AI config files selected for removal')
96
- return ctx
97
- }
98
-
99
- warn('Removing selected AI config files:')
100
- for (const f of selected) {
101
- info(' ' + f.replace(cwd + path.sep, ''))
102
- await fse.remove(f)
103
- }
104
- success('Removed existing AI config files')
105
-
106
- return ctx
107
- }
1
+ import { checkbox } from '@inquirer/prompts'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { findAiFiles } from '../../utils/copy.js'
6
+ import { header, info, success, warn } from '../../utils/exec.js'
7
+ import { MARKERS } from '../../utils/terminal.js'
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
+ const CLEAN_PRESET_PATH = path.resolve(__dirname, '../../presets/clean.json')
11
+ const cleanPreset = await fse.readJson(CLEAN_PRESET_PATH)
12
+
13
+ async function childrenExcludingPreserved(dir) {
14
+ const results = []
15
+ if (!await fse.pathExists(dir)) return results
16
+ const entries = await fse.readdir(dir)
17
+ for (const entry of entries) {
18
+ if (cleanPreset.preserveSubfolders.includes(entry)) continue
19
+ results.push(path.join(dir, entry))
20
+ }
21
+ return results
22
+ }
23
+
24
+ async function isPopulated(filePath) {
25
+ if (!await fse.pathExists(filePath)) return false
26
+ const content = await fse.readFile(filePath, 'utf-8')
27
+ const trimmed = content.trim()
28
+ if (!trimmed) return false
29
+ if (trimmed.startsWith('<!-- onboard-prompt')) return false
30
+ return true
31
+ }
32
+
33
+ async function hasOpenspecHistory(cwd) {
34
+ const changesDir = path.join(cwd, 'openspec', 'changes')
35
+ const archiveDir = path.join(cwd, 'openspec', 'archive')
36
+ if (await fse.pathExists(changesDir)) {
37
+ const entries = await fse.readdir(changesDir)
38
+ if (entries.length > 0) return true
39
+ }
40
+ if (await fse.pathExists(archiveDir)) {
41
+ const entries = await fse.readdir(archiveDir)
42
+ if (entries.length > 0) return true
43
+ }
44
+ return false
45
+ }
46
+
47
+ export async function cleanAiFiles() {
48
+ header('Step 2, Existing AI config files')
49
+
50
+ const cwd = process.cwd()
51
+ const ctx = {
52
+ hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
53
+ hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
54
+ hasOpenspec: await hasOpenspecHistory(cwd),
55
+ }
56
+
57
+ if (ctx.hasDesign) info('DESIGN.md exists and is populated, keeping it')
58
+ if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
59
+ if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
60
+
61
+ const flatFiles = await findAiFiles(cwd, cleanPreset.detectFiles)
62
+ const dirTargets = cleanPreset.directoryTargets
63
+ const dirEntries = []
64
+ for (const dirName of dirTargets) {
65
+ const dirPath = path.join(cwd, dirName)
66
+ const children = await childrenExcludingPreserved(dirPath)
67
+ dirEntries.push(...children)
68
+ }
69
+
70
+ const filteredFlat = flatFiles.filter(f => {
71
+ const rel = path.relative(cwd, f)
72
+ if (dirTargets.includes(rel)) return false
73
+ if (cleanPreset.preserve.some(p => rel === p || rel.startsWith(p + path.sep))) return false
74
+ return true
75
+ })
76
+
77
+ const allToRemove = [...filteredFlat, ...dirEntries]
78
+
79
+ if (allToRemove.length === 0) {
80
+ success('No existing AI config files to remove')
81
+ return ctx
82
+ }
83
+
84
+ const choices = allToRemove.map(f => ({
85
+ name: path.relative(cwd, f).replace(/\\/g, '/'),
86
+ value: f,
87
+ checked: true,
88
+ }))
89
+
90
+ const selected = await checkbox({
91
+ message: cleanPreset.selectionMessage,
92
+ choices,
93
+ })
94
+
95
+ if (!selected || selected.length === 0) {
96
+ success('No AI config files selected for removal')
97
+ return ctx
98
+ }
99
+
100
+ warn('Removing selected AI config files:')
101
+ for (const f of selected) {
102
+ info(`${MARKERS.EMPTY}${f.replace(cwd + path.sep, '')}`)
103
+ await fse.remove(f)
104
+ }
105
+ success('Removed existing AI config files')
106
+
107
+ return ctx
108
+ }
@@ -1,62 +1,63 @@
1
- import { execa } from 'execa'
2
- import fse from 'fs-extra'
3
- import path from 'path'
4
- import { createRequire } from 'node:module'
5
- import { header, success, warn } from '../../utils/exec.js'
6
-
7
- const require = createRequire(import.meta.url)
8
- const { version: onboardVersion } = require('../../../package.json')
9
-
10
- async function detectOpencodeVersion() {
11
- try {
12
- const result = await execa('opencode', ['--version'], { reject: false })
13
- if (result.exitCode !== 0) return null
14
- const output = (result.stdout || result.stderr || '').trim()
15
- return output || null
16
- } catch {
17
- return null
18
- }
19
- }
20
-
21
- export async function writeOnboardConfig(data) {
22
- header('Step 10, Writing onboarding metadata')
23
-
24
- const opencodeVersion = await detectOpencodeVersion()
25
- const target = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
26
-
27
- const payload = {
28
- schema: 1,
29
- generatedAt: new Date().toISOString(),
30
- onboardVersion,
31
- opencodeVersion,
32
- wizard: {
33
- platform: data.platform,
34
- sourceMode: data.sourceMode,
35
- sourceRoots: data.sourceRoots,
36
- maxConcurrentAgents: data.maxConcurrentAgents ?? 4,
37
- preserved: {
38
- design: !!data.hasDesign,
39
- architecture: !!data.hasArchitecture,
40
- openspec: !!data.hasOpenspec,
41
- },
42
- additionalSkillsProvider: data.additionalSkillsProvider,
43
- models: {
44
- plan: data.planModel,
45
- build: data.buildModel,
46
- fast: data.fastModel,
47
- },
48
- optionalTools: data.optionalTools ?? null,
49
- cavemanGuidance: data.cavemanGuidance ?? null,
50
- },
51
- note: 'Informational file only. Editing this file does not change runtime behavior.',
52
- }
53
-
54
- try {
55
- await fse.ensureDir(path.dirname(target))
56
- await fse.writeJson(target, payload, { spaces: 2 })
57
- success('Wrote .opencode/opencode-onboard.json')
58
- if (!opencodeVersion) warn('Could not detect opencode version, saved as null')
59
- } catch (err) {
60
- warn(`Could not write onboarding metadata: ${err.message}`)
61
- }
62
- }
1
+ import { execa } from 'execa'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { createRequire } from 'node:module'
5
+ import { header, success, warn } from '../../utils/exec.js'
6
+
7
+ const require = createRequire(import.meta.url)
8
+ const { version: onboardVersion } = require('../../../package.json')
9
+
10
+ async function detectOpencodeVersion() {
11
+ try {
12
+ const result = await execa('opencode', ['--version'], { reject: false })
13
+ if (result.exitCode !== 0) return null
14
+ const output = (result.stdout || result.stderr || '').trim()
15
+ return output || null
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ export async function writeOnboardConfig(data) {
22
+ header('Step 10, Writing onboarding metadata')
23
+
24
+ const opencodeVersion = await detectOpencodeVersion()
25
+ const target = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
26
+
27
+ const payload = {
28
+ schema: 1,
29
+ generatedAt: new Date().toISOString(),
30
+ onboardVersion,
31
+ opencodeVersion,
32
+ wizard: {
33
+ platform: data.platform,
34
+ sourceMode: data.sourceMode,
35
+ sourceRoots: data.sourceRoots,
36
+ maxConcurrentAgents: data.maxConcurrentAgents ?? 4,
37
+ preserved: {
38
+ design: !!data.hasDesign,
39
+ architecture: !!data.hasArchitecture,
40
+ openspec: !!data.hasOpenspec,
41
+ },
42
+ openspec: data.openspec,
43
+ additionalSkillsProvider: data.additionalSkillsProvider,
44
+ models: {
45
+ plan: data.planModel,
46
+ build: data.buildModel,
47
+ fast: data.fastModel,
48
+ },
49
+ optionalTools: data.optionalTools ?? null,
50
+ cavemanGuidance: data.cavemanGuidance ?? null,
51
+ },
52
+ note: 'Informational file only. Editing this file does not change runtime behavior.',
53
+ }
54
+
55
+ try {
56
+ await fse.ensureDir(path.dirname(target))
57
+ await fse.writeJson(target, payload, { spaces: 2 })
58
+ success('Wrote .opencode/opencode-onboard.json')
59
+ if (!opencodeVersion) warn('Could not detect opencode version, saved as null')
60
+ } catch (err) {
61
+ warn(`Could not write onboarding metadata: ${err.message}`)
62
+ }
63
+ }
@@ -1,60 +1,61 @@
1
- import { search } from '@inquirer/prompts'
2
- import fse from 'fs-extra'
3
- import path from 'path'
4
- import { fileURLToPath } from 'url'
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const MODELS_PRESET_PATH = path.resolve(__dirname, '../../presets/models.json');
8
- const modelsPreset = await fse.readJson(MODELS_PRESET_PATH);
9
-
10
- function costTier(input) {
11
- if (input === undefined || input === null) return '';
12
- const tier = modelsPreset.costTiers.find(t => t.max === undefined || input <= t.max);
13
- return tier ? ` ${tier.label}` : '';
14
- }
15
-
16
- function costTierDisplay(cost, canonicalCost) {
17
- return costTier(canonicalCost !== undefined ? canonicalCost : cost);
18
- }
19
-
20
- function formatPrice(price) {
21
- if (price === undefined || price === null) return '?';
22
- if (price === 0) return '$0 (subscription)';
23
- return `$${price}/M`;
24
- }
25
-
26
- export function buildDisplayModels(rawModels) {
27
- return rawModels.map(m => {
28
- const priceStr = formatPrice(m.cost);
29
- const canonicalNote = m.canonicalCost !== undefined
30
- ? ` · official price: ${formatPrice(m.canonicalCost)}/M`
31
- : '';
32
- return {
33
- ...m,
34
- label: `${m.name}${costTierDisplay(m.cost, m.canonicalCost)}, ${m.id}`,
35
- description: `${priceStr}${canonicalNote} · context: ${m.context ? (m.context / 1000) + 'k' : '?'}`,
36
- };
37
- });
38
- }
39
-
40
- export async function pickModel(message, models) {
41
- return await search({
42
- message,
43
- source: (input) => {
44
- const q = (input || '').toLowerCase();
45
- const filtered = q
46
- ? models.filter(m =>
47
- m.label.toLowerCase().includes(q) ||
48
- m.id.toLowerCase().includes(q)
49
- )
50
- : models;
51
- return filtered.slice(0, 50).map(m => ({
52
- name: m.label,
53
- value: m.id,
54
- description: m.description,
55
- }));
56
- },
57
- });
58
- }
59
-
60
- export { modelsPreset };
1
+ import { search } from '@inquirer/prompts'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const MODELS_PRESET_PATH = path.resolve(__dirname, '../../presets/models.json');
8
+ const modelsPreset = await fse.readJson(MODELS_PRESET_PATH);
9
+
10
+ function costTier(input) {
11
+ if (input === undefined || input === null) return '';
12
+ const tier = modelsPreset.costTiers.find(t => t.max === undefined || input <= t.max);
13
+ return tier ? ` ${tier.label}` : '';
14
+ }
15
+
16
+ function costTierDisplay(cost, canonicalCost) {
17
+ return costTier(canonicalCost !== undefined ? canonicalCost : cost);
18
+ }
19
+
20
+ function formatPrice(price) {
21
+ if (price === undefined || price === null) return '?';
22
+ if (price === 0) return '$0 (subscription)';
23
+ return `$${price}/M`;
24
+ }
25
+
26
+ export function buildDisplayModels(rawModels) {
27
+ return rawModels.map(m => {
28
+ const priceStr = formatPrice(m.cost);
29
+ const canonicalNote = m.canonicalCost !== undefined
30
+ ? ` · official price: ${formatPrice(m.canonicalCost)}/M`
31
+ : '';
32
+ const context = m.context ? `${m.context / 1000}k` : '?';
33
+ return {
34
+ ...m,
35
+ label: `${m.name}${costTierDisplay(m.cost, m.canonicalCost)}, ${m.id}`,
36
+ description: `${priceStr}${canonicalNote} · context: ${context}`,
37
+ };
38
+ });
39
+ }
40
+
41
+ export function pickModel(message, models) {
42
+ return search({
43
+ message,
44
+ source: (input) => {
45
+ const q = (input || '').toLowerCase();
46
+ const filtered = q
47
+ ? models.filter(m =>
48
+ m.label.toLowerCase().includes(q) ||
49
+ m.id.toLowerCase().includes(q)
50
+ )
51
+ : models;
52
+ return filtered.slice(0, 50).map(m => ({
53
+ name: m.label,
54
+ value: m.id,
55
+ description: m.description,
56
+ }));
57
+ },
58
+ });
59
+ }
60
+
61
+ export { modelsPreset };