tissues 0.6.0 → 0.6.2
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/README.md +78 -1
- package/package.json +4 -2
- package/src/cli.js +12 -1
- package/src/commands/ai.js +149 -173
- package/src/commands/config.js +143 -69
- package/src/commands/create.js +16 -9
- package/src/commands/create.test.js +381 -0
- package/src/commands/enhancements.js +282 -0
- package/src/commands/flush.test.js +299 -0
- package/src/commands/list.js +3 -2
- package/src/commands/providers.js +347 -0
- package/src/commands/providers.test.js +28 -0
- package/src/commands/storage.js +167 -0
- package/src/commands/sync.js +225 -0
- package/src/daemon/sync.js +189 -0
- package/src/lib/ai/adapters/claude-cli.js +55 -0
- package/src/lib/ai/adapters/cli-adapters.test.js +133 -0
- package/src/lib/ai/adapters/codex-cli.js +77 -0
- package/src/lib/ai/adapters/command.js +23 -13
- package/src/lib/ai/adapters/gemini-cli.js +55 -0
- package/src/lib/ai/adapters/openclaw.js +91 -0
- package/src/lib/ai/agent-actions.js +271 -0
- package/src/lib/ai/agent.js +323 -0
- package/src/lib/ai/body-template.js +15 -0
- package/src/lib/ai/discovery.js +89 -0
- package/src/lib/ai/discovery.test.js +74 -0
- package/src/lib/ai/enhance.js +48 -11
- package/src/lib/ai/enhancement-adapter.js +109 -0
- package/src/lib/ai/enhancement-adapter.test.js +188 -0
- package/src/lib/ai/index.js +2 -2
- package/src/lib/ai/pipeline.js +20 -2
- package/src/lib/ai/pipeline.test.js +257 -0
- package/src/lib/ai/prompt.test.js +30 -0
- package/src/lib/ai/router.js +118 -7
- package/src/lib/ai/router.test.js +481 -0
- package/src/lib/ai/steps.js +23 -3
- package/src/lib/ai/steps.test.js +335 -0
- package/src/lib/attribution.test.js +64 -0
- package/src/lib/cache.js +408 -0
- package/src/lib/db.js +42 -0
- package/src/lib/dedup.js +44 -48
- package/src/lib/dedup.test.js +227 -0
- package/src/lib/defaults.js +37 -1
- package/src/lib/defaults.test.js +217 -0
- package/src/lib/drafts-perf.test.js +203 -0
- package/src/lib/drafts.test.js +300 -0
- package/src/lib/enhancements.js +436 -0
- package/src/lib/enhancements.test.js +294 -0
- package/src/lib/gh.js +76 -10
- package/src/lib/safety.test.js +217 -0
- package/src/lib/storage.js +298 -0
- package/src/lib/templates.test.js +207 -0
package/src/commands/config.js
CHANGED
|
@@ -9,7 +9,7 @@ import { theme } from '../lib/theme.js'
|
|
|
9
9
|
import { listTemplates } from '../lib/templates.js'
|
|
10
10
|
import { pickRepo } from '../lib/repo-picker.js'
|
|
11
11
|
import { store } from '../lib/config.js'
|
|
12
|
-
import { listProviders } from '../lib/ai/index.js'
|
|
12
|
+
import { listProviders, listAllProviders } from '../lib/ai/index.js'
|
|
13
13
|
import { listModels as listOllamaModels } from '../lib/ai/adapters/ollama.js'
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
@@ -68,6 +68,8 @@ function coerceValue(dotKey, raw) {
|
|
|
68
68
|
throw new Error(`Expected true/false for ${dotKey}, got: ${raw}`)
|
|
69
69
|
}
|
|
70
70
|
if (raw === 'null') return null
|
|
71
|
+
// Auto-coerce pure numeric strings when no default type is known
|
|
72
|
+
if (defaultVal === undefined && /^\d+$/.test(raw)) return Number(raw)
|
|
71
73
|
return raw
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -135,11 +137,16 @@ const PROVIDER_LABELS = {
|
|
|
135
137
|
async function menuActiveProvider() {
|
|
136
138
|
const cfg = loadConfig()
|
|
137
139
|
const current = readUserConfig()
|
|
138
|
-
const
|
|
140
|
+
const allProviders = listAllProviders(cfg)
|
|
141
|
+
const builtIn = listProviders()
|
|
139
142
|
|
|
140
143
|
const defaultProvider = BUILT_IN_DEFAULTS.ai.provider
|
|
141
144
|
const choices = [
|
|
142
|
-
...
|
|
145
|
+
...allProviders.map((p) => {
|
|
146
|
+
const isCustom = !builtIn.includes(p)
|
|
147
|
+
const label = isCustom ? `${p} ${dim('(custom)')}` : (PROVIDER_LABELS[p] || p)
|
|
148
|
+
return { name: label, value: p }
|
|
149
|
+
}),
|
|
143
150
|
{ name: 'None (disable AI)', value: 'none' },
|
|
144
151
|
{ name: dim(`Restore default (${defaultProvider})`), value: 'restore' },
|
|
145
152
|
{ name: dim('Back'), value: 'back' },
|
|
@@ -166,6 +173,13 @@ async function menuActiveProvider() {
|
|
|
166
173
|
updated = await configureOpenAICompat(updated)
|
|
167
174
|
} else if (provider === 'command') {
|
|
168
175
|
updated = await configureCommand(updated)
|
|
176
|
+
} else if (!builtIn.includes(provider)) {
|
|
177
|
+
// Named CLI command — just set as default provider
|
|
178
|
+
updated = setNestedValue(updated, 'ai.enabled', true)
|
|
179
|
+
updated = setNestedValue(updated, 'ai.provider', provider)
|
|
180
|
+
writeUserConfig(updated)
|
|
181
|
+
const cmd = cfg.ai?.providers?.[provider]?.command || cfg.ai?.commands?.[provider]?.command || '(not set)'
|
|
182
|
+
console.log(green(` ✔ Default provider: ${provider} ${dim(`→ ${cmd}`)}`))
|
|
169
183
|
} else {
|
|
170
184
|
updated = setNestedValue(updated, 'ai.enabled', true)
|
|
171
185
|
updated = setNestedValue(updated, 'ai.provider', provider)
|
|
@@ -518,7 +532,8 @@ function formatRoute(rule) {
|
|
|
518
532
|
const match = Object.entries(rule.match || {})
|
|
519
533
|
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(',') : v}`)
|
|
520
534
|
.join(', ')
|
|
521
|
-
|
|
535
|
+
const enhTag = rule.enhancements?.length ? dim(` [${rule.enhancements.join(',')}]`) : ''
|
|
536
|
+
return `${match} ${dim('->')} ${rule.provider || 'default'}${rule.model ? dim(` (${rule.model})`) : ''}${enhTag}`
|
|
522
537
|
}
|
|
523
538
|
|
|
524
539
|
async function editRoute(rule) {
|
|
@@ -571,8 +586,10 @@ async function editRoute(rule) {
|
|
|
571
586
|
}
|
|
572
587
|
|
|
573
588
|
// Provider
|
|
574
|
-
const
|
|
575
|
-
|
|
589
|
+
const allProviders = listAllProviders(cfg)
|
|
590
|
+
const builtInSet = new Set(listProviders())
|
|
591
|
+
const providerChoices = allProviders.map((p) => ({
|
|
592
|
+
name: builtInSet.has(p) ? (p.charAt(0).toUpperCase() + p.slice(1)) : `${p} ${dim('(custom)')}`,
|
|
576
593
|
value: p,
|
|
577
594
|
}))
|
|
578
595
|
const provider = await promptOrBack(() =>
|
|
@@ -591,7 +608,21 @@ async function editRoute(rule) {
|
|
|
591
608
|
)
|
|
592
609
|
if (model === Symbol.for('back')) return null
|
|
593
610
|
|
|
594
|
-
|
|
611
|
+
// Enhancements filter (optional)
|
|
612
|
+
const enhRaw = await promptOrBack(() =>
|
|
613
|
+
input({
|
|
614
|
+
message: 'Enhancements (comma-separated, enter to skip)',
|
|
615
|
+
default: rule?.enhancements?.join(', ') || '',
|
|
616
|
+
theme,
|
|
617
|
+
}),
|
|
618
|
+
)
|
|
619
|
+
if (enhRaw === Symbol.for('back')) return null
|
|
620
|
+
|
|
621
|
+
const enhancements = enhRaw
|
|
622
|
+
? enhRaw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
623
|
+
: undefined
|
|
624
|
+
|
|
625
|
+
return { match, provider, model: model || undefined, enhancements: enhancements?.length ? enhancements : undefined }
|
|
595
626
|
}
|
|
596
627
|
|
|
597
628
|
async function menuRoutingRules() {
|
|
@@ -660,87 +691,130 @@ async function menuRoutingRules() {
|
|
|
660
691
|
}
|
|
661
692
|
|
|
662
693
|
// ---------------------------------------------------------------------------
|
|
663
|
-
// Submenu:
|
|
694
|
+
// Submenu: Custom Providers (named command providers)
|
|
664
695
|
// ---------------------------------------------------------------------------
|
|
665
696
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
697
|
+
/** Migrate ai.commands → ai.providers on disk if needed. */
|
|
698
|
+
function migrateProvidersOnDisk(current) {
|
|
699
|
+
if (!current.ai) return current
|
|
700
|
+
let updated = current
|
|
701
|
+
if (current.ai.commands) {
|
|
702
|
+
const existing = current.ai.providers || {}
|
|
703
|
+
updated = setNestedValue(updated, 'ai.providers', { ...current.ai.commands, ...existing })
|
|
704
|
+
const ai = { ...updated.ai }
|
|
705
|
+
delete ai.commands
|
|
706
|
+
updated = { ...updated, ai }
|
|
707
|
+
}
|
|
708
|
+
return updated
|
|
676
709
|
}
|
|
677
710
|
|
|
678
|
-
async function
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
{
|
|
686
|
-
name: `${'Pipeline'.padEnd(18)} ${enabled ? green('enabled') : dim('disabled')}`,
|
|
687
|
-
value: 'toggle',
|
|
688
|
-
},
|
|
689
|
-
]
|
|
690
|
-
|
|
691
|
-
if (enabled) {
|
|
692
|
-
for (const name of STEP_NAMES) {
|
|
693
|
-
const setting = pipeline.steps?.[name] || 'auto'
|
|
694
|
-
const color = setting === 'always' ? green : setting === 'never' ? red : dim
|
|
695
|
-
choices.push({
|
|
696
|
-
name: ` ${(STEP_LABELS[name] || name).padEnd(22)} ${color(setting)}`,
|
|
697
|
-
value: name,
|
|
698
|
-
})
|
|
699
|
-
}
|
|
711
|
+
async function menuProviders() {
|
|
712
|
+
// One-time disk migration when user opens the menu
|
|
713
|
+
{
|
|
714
|
+
const current = readUserConfig()
|
|
715
|
+
if (current.ai?.commands) {
|
|
716
|
+
const migrated = migrateProvidersOnDisk(current)
|
|
717
|
+
writeUserConfig(migrated)
|
|
700
718
|
}
|
|
719
|
+
}
|
|
701
720
|
|
|
702
|
-
|
|
721
|
+
while (true) {
|
|
722
|
+
const cfg = loadConfig()
|
|
723
|
+
const providers = cfg.ai?.providers || cfg.ai?.commands || {}
|
|
724
|
+
const names = Object.keys(providers)
|
|
725
|
+
|
|
726
|
+
const choices = names.map((name) => {
|
|
727
|
+
const entry = providers[name]
|
|
728
|
+
const cmd = entry.command || dim('(no command)')
|
|
729
|
+
const timeout = entry.timeout ? dim(` ${entry.timeout}ms`) : ''
|
|
730
|
+
return { name: `${name.padEnd(18)} ${dim(cmd)}${timeout}`, value: name }
|
|
731
|
+
})
|
|
732
|
+
choices.push({ name: green('+ Add provider'), value: 'add' })
|
|
703
733
|
choices.push({ name: dim('Back'), value: 'back' })
|
|
704
734
|
|
|
705
|
-
const chosen = await promptOrBack(() => select({ message: '
|
|
735
|
+
const chosen = await promptOrBack(() => select({ message: 'Custom Providers', choices, theme }))
|
|
706
736
|
if (chosen === Symbol.for('back') || chosen === 'back') return
|
|
707
737
|
|
|
708
|
-
if (chosen === '
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
738
|
+
if (chosen === 'add') {
|
|
739
|
+
const name = await promptOrBack(() =>
|
|
740
|
+
input({ message: 'Provider name (e.g. my-gemini)', theme }),
|
|
741
|
+
)
|
|
742
|
+
if (name === Symbol.for('back') || !name) continue
|
|
743
|
+
const key = name.trim().toLowerCase().replace(/\s+/g, '-')
|
|
744
|
+
if (!key) continue
|
|
745
|
+
if (providers[key]) {
|
|
746
|
+
console.log(yellow(` "${key}" already exists — select it to edit`))
|
|
747
|
+
continue
|
|
748
|
+
}
|
|
749
|
+
const cmd = await promptOrBack(() =>
|
|
750
|
+
input({ message: 'Shell command to run', theme }),
|
|
751
|
+
)
|
|
752
|
+
if (cmd === Symbol.for('back') || !cmd) continue
|
|
753
|
+
const timeoutRaw = await promptOrBack(() =>
|
|
754
|
+
input({ message: 'Timeout ms (enter for default 60s)', default: '', theme }),
|
|
755
|
+
)
|
|
756
|
+
if (timeoutRaw === Symbol.for('back')) continue
|
|
715
757
|
|
|
716
|
-
if (chosen === 'restore') {
|
|
717
758
|
const current = readUserConfig()
|
|
718
|
-
|
|
759
|
+
let updated = setNestedValue(current, `ai.providers.${key}.command`, cmd)
|
|
760
|
+
if (timeoutRaw) {
|
|
761
|
+
const ms = Number(timeoutRaw)
|
|
762
|
+
if (!isNaN(ms) && ms > 0) {
|
|
763
|
+
updated = setNestedValue(updated, `ai.providers.${key}.timeout`, ms)
|
|
764
|
+
}
|
|
765
|
+
}
|
|
719
766
|
writeUserConfig(updated)
|
|
720
|
-
console.log(green(
|
|
767
|
+
console.log(green(` ✔ Added: ${key} → ${cmd}`))
|
|
721
768
|
continue
|
|
722
769
|
}
|
|
723
770
|
|
|
724
|
-
//
|
|
725
|
-
const
|
|
771
|
+
// Edit/delete existing provider
|
|
772
|
+
const entry = providers[chosen]
|
|
773
|
+
const action = await promptOrBack(() =>
|
|
726
774
|
select({
|
|
727
|
-
message: `${
|
|
775
|
+
message: `${chosen}: ${entry.command || '(no command)'}`,
|
|
728
776
|
choices: [
|
|
729
|
-
{ name: '
|
|
730
|
-
{ name: '
|
|
731
|
-
{ name: 'never — skip this step', value: 'never' },
|
|
777
|
+
{ name: 'Edit', value: 'edit' },
|
|
778
|
+
{ name: red('Delete'), value: 'delete' },
|
|
732
779
|
{ name: dim('Back'), value: 'back' },
|
|
733
780
|
],
|
|
734
|
-
default: pipeline.steps?.[chosen] || 'auto',
|
|
735
781
|
theme,
|
|
736
782
|
}),
|
|
737
783
|
)
|
|
738
|
-
if (
|
|
784
|
+
if (action === Symbol.for('back') || action === 'back') continue
|
|
739
785
|
|
|
740
786
|
const current = readUserConfig()
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
787
|
+
if (action === 'delete') {
|
|
788
|
+
const provs = { ...(current.ai?.providers || {}) }
|
|
789
|
+
delete provs[chosen]
|
|
790
|
+
const updated = setNestedValue(current, 'ai.providers', provs)
|
|
791
|
+
writeUserConfig(updated)
|
|
792
|
+
console.log(green(` ✔ Deleted: ${chosen}`))
|
|
793
|
+
} else {
|
|
794
|
+
const cmd = await promptOrBack(() =>
|
|
795
|
+
input({ message: 'Shell command', default: entry.command || '', theme }),
|
|
796
|
+
)
|
|
797
|
+
if (cmd === Symbol.for('back')) continue
|
|
798
|
+
const timeoutRaw = await promptOrBack(() =>
|
|
799
|
+
input({ message: 'Timeout ms (enter for default)', default: entry.timeout ? String(entry.timeout) : '', theme }),
|
|
800
|
+
)
|
|
801
|
+
if (timeoutRaw === Symbol.for('back')) continue
|
|
802
|
+
|
|
803
|
+
let updated = current
|
|
804
|
+
if (cmd) updated = setNestedValue(updated, `ai.providers.${chosen}.command`, cmd)
|
|
805
|
+
if (timeoutRaw) {
|
|
806
|
+
const ms = Number(timeoutRaw)
|
|
807
|
+
if (!isNaN(ms) && ms > 0) {
|
|
808
|
+
updated = setNestedValue(updated, `ai.providers.${chosen}.timeout`, ms)
|
|
809
|
+
}
|
|
810
|
+
} else {
|
|
811
|
+
// Clear timeout if empty
|
|
812
|
+
const provs = { ...(updated.ai?.providers || {}) }
|
|
813
|
+
if (provs[chosen]) { delete provs[chosen].timeout; updated = setNestedValue(updated, 'ai.providers', provs) }
|
|
814
|
+
}
|
|
815
|
+
writeUserConfig(updated)
|
|
816
|
+
console.log(green(` ✔ Updated: ${chosen}`))
|
|
817
|
+
}
|
|
744
818
|
}
|
|
745
819
|
}
|
|
746
820
|
|
|
@@ -910,7 +984,7 @@ async function runWizard() {
|
|
|
910
984
|
template: cfg.templates?.default || 'default',
|
|
911
985
|
safety: `${cfg.safety?.maxPerHour || '?'}/hr, burst ${cfg.safety?.burstLimit || '?'}`,
|
|
912
986
|
routes: `${(cfg.ai?.routes || []).length} rule${(cfg.ai?.routes || []).length === 1 ? '' : 's'}`,
|
|
913
|
-
|
|
987
|
+
providers: `${Object.keys(cfg.ai?.providers || cfg.ai?.commands || {}).length} registered`,
|
|
914
988
|
backups: `${getBackupFiles().length} saved`,
|
|
915
989
|
}
|
|
916
990
|
|
|
@@ -921,8 +995,8 @@ async function runWizard() {
|
|
|
921
995
|
{ name: `${'API Keys'.padEnd(18)} ${dim(summaries.keys)}`, value: 'keys' },
|
|
922
996
|
{ name: `${'Token Budgets'.padEnd(18)} ${dim(summaries.budgets)}`, value: 'budgets' },
|
|
923
997
|
{ name: `${'Safety Limits'.padEnd(18)} ${dim(summaries.safety)}`, value: 'safety' },
|
|
924
|
-
{ name: `${'Routing Rules'.padEnd(18)} ${dim(summaries.routes)}`,
|
|
925
|
-
{ name: `${'
|
|
998
|
+
{ name: `${'Routing Rules'.padEnd(18)} ${dim(summaries.routes)}`, value: 'routes' },
|
|
999
|
+
{ name: `${'Custom Providers'.padEnd(18)} ${dim(summaries.providers)}`, value: 'providers' },
|
|
926
1000
|
{ name: `${'Backup & Restore'.padEnd(18)} ${dim(summaries.backups)}`, value: 'backup' },
|
|
927
1001
|
{ name: dim('Done'), value: 'done' },
|
|
928
1002
|
]
|
|
@@ -944,9 +1018,9 @@ async function runWizard() {
|
|
|
944
1018
|
case 'budgets': await menuTokenBudgets(); break
|
|
945
1019
|
case 'template': await menuDefaultTemplate(); break
|
|
946
1020
|
case 'safety': await menuSafety(); break
|
|
947
|
-
case 'routes':
|
|
948
|
-
case '
|
|
949
|
-
case 'backup':
|
|
1021
|
+
case 'routes': await menuRoutingRules(); break
|
|
1022
|
+
case 'providers': await menuProviders(); break
|
|
1023
|
+
case 'backup': await menuBackupRestore(); break
|
|
950
1024
|
}
|
|
951
1025
|
} catch (err) {
|
|
952
1026
|
if (isCancelled(err)) continue
|
package/src/commands/create.js
CHANGED
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
createIssue,
|
|
16
16
|
verifyIssue,
|
|
17
17
|
listLabels,
|
|
18
|
-
listIssues,
|
|
19
18
|
createLabel,
|
|
20
19
|
addLabelsToIssue,
|
|
21
20
|
uploadImageToRepo,
|
|
22
21
|
} from '../lib/gh.js'
|
|
22
|
+
import { ensureFresh } from '../lib/cache.js'
|
|
23
23
|
import { enhance, checkAvailable } from '../lib/ai/index.js'
|
|
24
24
|
import { runEnhancePipeline } from '../lib/ai/enhance.js'
|
|
25
25
|
import { clipboardHasImage, saveClipboardImage } from '../lib/clipboard.js'
|
|
@@ -401,11 +401,15 @@ export async function runCreate(opts) {
|
|
|
401
401
|
let body = renderedTemplate
|
|
402
402
|
let pipelineResult = null
|
|
403
403
|
if (opts.enhance !== false && !opts.dryRun) {
|
|
404
|
+
const enhancementNames = opts.enhancements
|
|
405
|
+
? opts.enhancements.split(',').map((s) => s.trim()).filter(Boolean)
|
|
406
|
+
: undefined
|
|
404
407
|
const aiContext = {
|
|
405
408
|
template: templateName,
|
|
406
409
|
labels: opts.labels ? opts.labels.split(',').map((l) => l.trim()).filter(Boolean) : [],
|
|
407
410
|
provider: opts.provider,
|
|
408
411
|
model: opts.model,
|
|
412
|
+
enhancements: enhancementNames,
|
|
409
413
|
}
|
|
410
414
|
if (checkAvailable(config, aiContext)) {
|
|
411
415
|
const pipelineConfig = config.ai?.pipeline || {}
|
|
@@ -416,11 +420,11 @@ export async function runCreate(opts) {
|
|
|
416
420
|
// Multi-step pipeline enhancement
|
|
417
421
|
let aiSpinner = ora('Enhancing with AI pipeline...').start()
|
|
418
422
|
try {
|
|
419
|
-
// Gather existing issues and repo labels for pipeline context
|
|
423
|
+
// Gather existing issues and repo labels for pipeline context (from cache)
|
|
420
424
|
let existingIssues = []
|
|
421
425
|
let repoLabels = []
|
|
422
|
-
try { existingIssues =
|
|
423
|
-
try { repoLabels =
|
|
426
|
+
try { existingIssues = ensureFresh(repo, 'issues', { state: 'open', acceptStale: true }) } catch { /* non-fatal */ }
|
|
427
|
+
try { repoLabels = ensureFresh(repo, 'labels', { acceptStale: true }) } catch { /* non-fatal */ }
|
|
424
428
|
|
|
425
429
|
// When --title is explicit, skip triage (no rawInput to analyze)
|
|
426
430
|
const rawInput = opts.title ? '' : (opts._rawInput || '')
|
|
@@ -435,7 +439,10 @@ export async function runCreate(opts) {
|
|
|
435
439
|
existingIssues,
|
|
436
440
|
repoLabels,
|
|
437
441
|
}, {
|
|
438
|
-
onStepStart(step) {
|
|
442
|
+
onStepStart(step) {
|
|
443
|
+
if (!aiSpinner.isSpinning) aiSpinner = ora().start()
|
|
444
|
+
aiSpinner.text = `${step.displayName}...`
|
|
445
|
+
},
|
|
439
446
|
onStepDone(step, ctx) {
|
|
440
447
|
let detail = ''
|
|
441
448
|
if (step.name === 'triage') detail = ctx.title ? `: ${ctx.title}` : ''
|
|
@@ -445,13 +452,12 @@ export async function runCreate(opts) {
|
|
|
445
452
|
if (step.name === 'risk') detail = ctx.risk != null ? `: ${ctx.risk}/10` : ''
|
|
446
453
|
if (step.name === 'labels') detail = ctx.aiLabels?.length ? `: ${ctx.aiLabels.join(', ')}` : ''
|
|
447
454
|
aiSpinner.succeed(`${step.displayName}${detail}`)
|
|
448
|
-
aiSpinner = ora('').start()
|
|
449
455
|
},
|
|
450
456
|
onStepSkip(step) { /* silent */ },
|
|
451
457
|
onStepFail(step, err) {
|
|
458
|
+
if (!aiSpinner.isSpinning) aiSpinner = ora().start()
|
|
452
459
|
aiSpinner.warn(`${step.displayName} failed`)
|
|
453
460
|
console.warn(dim(` ${err.message}`))
|
|
454
|
-
aiSpinner = ora('').start()
|
|
455
461
|
},
|
|
456
462
|
}, aiContext)
|
|
457
463
|
|
|
@@ -624,11 +630,11 @@ export async function runCreate(opts) {
|
|
|
624
630
|
|
|
625
631
|
if (labels.length > 0) {
|
|
626
632
|
try {
|
|
627
|
-
const repoLabelNames =
|
|
633
|
+
const repoLabelNames = ensureFresh(repo, 'labels', { acceptStale: true })
|
|
628
634
|
existingLabels = labels.filter((l) => repoLabelNames.includes(l))
|
|
629
635
|
missingLabels = labels.filter((l) => !repoLabelNames.includes(l))
|
|
630
636
|
} catch {
|
|
631
|
-
// If
|
|
637
|
+
// If label check fails, proceed with all labels (original behavior)
|
|
632
638
|
}
|
|
633
639
|
}
|
|
634
640
|
|
|
@@ -760,6 +766,7 @@ export const createCommand = new Command('create')
|
|
|
760
766
|
.option('--no-enhance', 'Skip AI enhancement, use rendered template as-is')
|
|
761
767
|
.option('--pipeline', 'Force multi-step AI pipeline even if config disabled')
|
|
762
768
|
.option('--no-pipeline', 'Force single-shot AI enhancement even if pipeline enabled')
|
|
769
|
+
.option('--enhancements <names>', 'Comma-separated enhancement names to run (e.g. "context,risk")')
|
|
763
770
|
.option('--provider <name>', 'AI provider override (anthropic, openai, gemini)')
|
|
764
771
|
.option('--model <name>', 'AI model override')
|
|
765
772
|
.option(
|