dsh-tacit 0.2.2 → 0.2.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/README.md +21 -0
- package/client/client.js +85 -19
- package/docs/README.md +3 -3
- package/docs/README.zh.md +4 -3
- package/lib/analyze.js +175 -19
- package/lib/index.js +13 -0
- package/lib/routes.js +1 -1
- package/lib/schema.js +17 -1
- package/lib/service.js +200 -50
- package/lib/store.js +1 -0
- package/package.json +11 -5
package/lib/analyze.js
CHANGED
|
@@ -40,6 +40,25 @@ export function clipSafe(value, max) {
|
|
|
40
40
|
return text.slice(0, end)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Clip a directive to `max` characters without ever cutting mid-word: prefer
|
|
45
|
+
* the last sentence end at or after `min`, else the last space, and mark a
|
|
46
|
+
* mid-sentence cut with an ellipsis. Text within the limit is returned as is.
|
|
47
|
+
*/
|
|
48
|
+
export function clipDirective(value, max = DIRECTIVE_MAX_CHARS, min = 60) {
|
|
49
|
+
const text = (typeof value === 'string' ? value : '').trim()
|
|
50
|
+
if (text.length <= max) return text
|
|
51
|
+
const head = clipSafe(text, max)
|
|
52
|
+
const sentenceEnd = Math.max(...['. ', '! ', '? ', '; ', '。', '!', '?', ';'].map((mark) => {
|
|
53
|
+
const at = head.lastIndexOf(mark)
|
|
54
|
+
return at >= min ? at + mark.trim().length : -1
|
|
55
|
+
}))
|
|
56
|
+
if (sentenceEnd > 0) return head.slice(0, sentenceEnd).trim()
|
|
57
|
+
const space = head.lastIndexOf(' ')
|
|
58
|
+
const cut = space >= min ? head.slice(0, space) : clipSafe(head, max - 1)
|
|
59
|
+
return cut.replace(/[\s,;:–—-]+$/u, '') + '…'
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
// ── Structured output: the model answers by CALLING one tool whose arguments
|
|
44
63
|
// are the payload (the harness has no JSON mode; tool arguments are the
|
|
45
64
|
// reliable structured channel). Text JSON is still accepted as a fallback.
|
|
@@ -88,11 +107,31 @@ export const DIRECTIVE_TOOL = {
|
|
|
88
107
|
parameters: {
|
|
89
108
|
type: 'object',
|
|
90
109
|
properties: {
|
|
91
|
-
directives: {
|
|
110
|
+
directives: {
|
|
111
|
+
type: 'array',
|
|
112
|
+
minItems: 1,
|
|
113
|
+
maxItems: 4,
|
|
114
|
+
items: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
text: { type: 'string', maxLength: 220, description: 'one sentence, at most 25 words' },
|
|
118
|
+
workspace: { type: 'string', description: 'only when the habit shows up in exactly one workspace: that workspace name as written in the evidence' },
|
|
119
|
+
},
|
|
120
|
+
required: ['text'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
92
123
|
},
|
|
93
124
|
required: ['directives'],
|
|
94
125
|
},
|
|
95
126
|
}
|
|
127
|
+
export const MAX_WORKSPACE_DIRECTIVES = 4
|
|
128
|
+
|
|
129
|
+
/** The last path segment of a workspace directory — what a person calls the project. */
|
|
130
|
+
export function workspaceLabel(cwd) {
|
|
131
|
+
if (typeof cwd !== 'string' || cwd.length === 0) return ''
|
|
132
|
+
const parts = cwd.split(/[\\/]+/).filter((part) => part.length > 0)
|
|
133
|
+
return parts.length > 0 ? parts[parts.length - 1] : ''
|
|
134
|
+
}
|
|
96
135
|
export const DIRECTIVE_MAX_TOKENS = 1500
|
|
97
136
|
export const DIRECTIVE_TIMEOUT_MS = 30000
|
|
98
137
|
export const MAX_DIRECTIVES = 8
|
|
@@ -137,7 +176,7 @@ export function buildEnrichUserText({ draft, profile, recentContext }) {
|
|
|
137
176
|
.filter((entry) => entry !== null && typeof entry === 'object' && entry.enabled !== false && typeof entry.text === 'string')
|
|
138
177
|
if (directives.length > 0) {
|
|
139
178
|
lines.push('=== WHAT THE COACH KNOWS ABOUT THIS USER ===')
|
|
140
|
-
for (const entry of directives.slice(0, MAX_DIRECTIVES)) lines.push('- ' +
|
|
179
|
+
for (const entry of directives.slice(0, MAX_DIRECTIVES)) lines.push('- ' + clipDirective(entry.text))
|
|
141
180
|
lines.push('')
|
|
142
181
|
}
|
|
143
182
|
const patterns = Array.isArray(profile?.patterns) ? profile.patterns.slice(0, 6) : []
|
|
@@ -226,6 +265,47 @@ const improveShape = z.object({
|
|
|
226
265
|
rationale: z.string().default(''),
|
|
227
266
|
})
|
|
228
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Learning from a recovery: the turn went cleanly right after a messy one in
|
|
270
|
+
* the same conversation. One small call names what the user supplied this
|
|
271
|
+
* time, in the same problem taxonomy, so the distiller knows what this user
|
|
272
|
+
* CAN state when reminded.
|
|
273
|
+
*/
|
|
274
|
+
export const GOOD_TOOL = {
|
|
275
|
+
name: 'report',
|
|
276
|
+
description: 'What the clean prompt supplied that the previous, messy one lacked.',
|
|
277
|
+
parameters: {
|
|
278
|
+
type: 'object',
|
|
279
|
+
properties: {
|
|
280
|
+
strengths: {
|
|
281
|
+
type: 'array',
|
|
282
|
+
maxItems: 4,
|
|
283
|
+
items: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
kind: { type: 'string', description: 'same categories as problems: missing-constraints|ambiguous-goal|missing-context|wrong-scope|...' },
|
|
287
|
+
what: { type: 'string', description: 'one sentence: what the prompt included this time' },
|
|
288
|
+
},
|
|
289
|
+
required: ['kind', 'what'],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
lesson: { type: 'string', description: 'one sentence, about the user: what they included the second time that made the difference' },
|
|
293
|
+
},
|
|
294
|
+
required: ['strengths', 'lesson'],
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
export const GOOD_SYSTEM_PROMPT = [
|
|
298
|
+
'You coach prompt writing inside DeepSeek Harness. The previous turn of this',
|
|
299
|
+
'conversation went badly (retries, tool errors, a correction).',
|
|
300
|
+
'This turn went well. Compare the two prompts and the trajectory: what did the user',
|
|
301
|
+
'INCLUDE this time that the earlier prompt lacked — a file path, a constraint,',
|
|
302
|
+
'a scope, an acceptance criterion, an example? Report 1-4 strengths in the same',
|
|
303
|
+
'categories used for problems, and ONE sentence about the user ("They fix',
|
|
304
|
+
'wandering by naming the target file up front."). If the clean turn was',
|
|
305
|
+
'trivially easy or merely a continuation, return an empty strengths list and',
|
|
306
|
+
'an empty lesson. Never praise; state facts. Reply in the language of the prompt.',
|
|
307
|
+
].join('\n')
|
|
308
|
+
|
|
229
309
|
export const ANALYSIS_SYSTEM_PROMPT = [
|
|
230
310
|
'You are a strict but friendly prompt-engineering coach inside DeepSeek Harness.',
|
|
231
311
|
'You are given ONE past user prompt plus a digest of everything that happened',
|
|
@@ -354,13 +434,23 @@ export const DIRECTIVE_SYSTEM_PROMPT = [
|
|
|
354
434
|
'"continue" — that is fine; the conversation is the context).',
|
|
355
435
|
'Directives must GENERALIZE across future tasks: never mention a specific',
|
|
356
436
|
'task, file, feature, number, or test from one past prompt — describe the',
|
|
357
|
-
'habit and the compensation.
|
|
358
|
-
'
|
|
437
|
+
'habit and the compensation. One exception: when every piece of evidence for',
|
|
438
|
+
'a habit carries the same [workspace: name] tag, return that directive with',
|
|
439
|
+
'"workspace" set to exactly that name; it may then refer to that project\'s',
|
|
440
|
+
'layout ("check apps/web first"). Leave "workspace" out for everything else.',
|
|
441
|
+
'Prefer the most frequent habits. 2-4 directives,',
|
|
442
|
+
'ONE sentence each of at most 25 words (under 180 characters), imperative',
|
|
443
|
+
'mood, addressed to the agent. A directive that needs a second sentence is two',
|
|
444
|
+
'directives or too specific. You are writing',
|
|
359
445
|
'the COMPLETE new set: keep existing directives that still hold (reworded if',
|
|
360
446
|
'sharper), drop ones that were one-off, add what is missing. No preamble.',
|
|
361
447
|
].join('\n')
|
|
362
448
|
|
|
363
|
-
export function buildDirectiveUserText(profile, recentReports = []) {
|
|
449
|
+
export function buildDirectiveUserText(profile, recentReports = [], { labelOf = workspaceLabel } = {}) {
|
|
450
|
+
const tagOf = (cwd) => {
|
|
451
|
+
const label = typeof cwd === 'string' && cwd.length > 0 ? labelOf(cwd) : ''
|
|
452
|
+
return label.length > 0 ? '[workspace: ' + label + '] ' : ''
|
|
453
|
+
}
|
|
364
454
|
const lines = ['=== RECURRING PROMPT HABITS (kind, times seen, latest example) ===']
|
|
365
455
|
const patterns = Array.isArray(profile?.patterns) ? profile.patterns.slice(0, 12) : []
|
|
366
456
|
if (patterns.length === 0) lines.push('(none yet)')
|
|
@@ -373,9 +463,27 @@ export function buildDirectiveUserText(profile, recentReports = []) {
|
|
|
373
463
|
if (corrections.length > 0) {
|
|
374
464
|
lines.push('', '=== RECENT CORRECTIONS (prompt → what the user said next) ===')
|
|
375
465
|
for (const report of corrections) {
|
|
376
|
-
lines.push('- "' + clipSafe(String(report.promptExcerpt ?? ''), 120) + '" → "' + clipSafe(report.followUp, 200) + '"')
|
|
466
|
+
lines.push('- ' + tagOf(report.cwd) + '"' + clipSafe(String(report.promptExcerpt ?? ''), 120) + '" → "' + clipSafe(report.followUp, 200) + '"')
|
|
377
467
|
}
|
|
378
468
|
}
|
|
469
|
+
const lessons = (Array.isArray(recentReports) ? recentReports : [])
|
|
470
|
+
.filter((report) => typeof report?.lesson === 'string' && report.lesson.length > 0)
|
|
471
|
+
.slice(-5)
|
|
472
|
+
if (lessons.length > 0) {
|
|
473
|
+
lines.push('', '=== WHAT WORKED (a clean prompt right after a messy turn — what the user included this time) ===')
|
|
474
|
+
for (const report of lessons) {
|
|
475
|
+
lines.push('- ' + tagOf(report.cwd) + '"' + clipSafe(String(report.promptExcerpt ?? ''), 120) + '": ' + clipSafe(report.lesson, 300))
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const byWorkspace = new Map()
|
|
479
|
+
for (const report of Array.isArray(recentReports) ? recentReports : []) {
|
|
480
|
+
const label = typeof report?.cwd === 'string' && report.cwd.length > 0 ? labelOf(report.cwd) : ''
|
|
481
|
+
if (label.length > 0) byWorkspace.set(label, (byWorkspace.get(label) ?? 0) + 1)
|
|
482
|
+
}
|
|
483
|
+
if (byWorkspace.size > 0) {
|
|
484
|
+
lines.push('', '=== WORKSPACES IN THE RECENT ANALYSES (name, analyses) ===')
|
|
485
|
+
for (const [label, count] of byWorkspace) lines.push('- ' + label + ' (' + count + ')')
|
|
486
|
+
}
|
|
379
487
|
const rules = Array.isArray(profile?.styleRules) ? profile.styleRules.filter((rule) => typeof rule?.rule === 'string' && rule.rule.length > 0) : []
|
|
380
488
|
if (rules.length > 0) {
|
|
381
489
|
lines.push('', '=== STYLE RULES THE USER CONFIRMED ===')
|
|
@@ -384,9 +492,9 @@ export function buildDirectiveUserText(profile, recentReports = []) {
|
|
|
384
492
|
const existing = Array.isArray(profile?.directives) ? profile.directives.filter((entry) => typeof entry?.text === 'string' && entry.text.length > 0) : []
|
|
385
493
|
if (existing.length > 0) {
|
|
386
494
|
lines.push('', '=== CURRENT DIRECTIVES (keep the ones that still hold) ===')
|
|
387
|
-
for (const entry of existing) lines.push('- ' +
|
|
495
|
+
for (const entry of existing) lines.push('- ' + clipDirective(entry.text) + (typeof entry.workspace === 'string' && entry.workspace.length > 0 ? ' [workspace: ' + labelOf(entry.workspace) + ']' : ''))
|
|
388
496
|
}
|
|
389
|
-
lines.push('', 'Write 2-4 directives for the agent about this user.')
|
|
497
|
+
lines.push('', 'Write 2-4 directives for the agent about this user. A habit the user has', 'shown they can fix themselves is still worth a directive: the agent should', 'compensate for it when it is missing, not ask.')
|
|
390
498
|
return lines.join('\n')
|
|
391
499
|
}
|
|
392
500
|
|
|
@@ -405,30 +513,42 @@ export function classifyDirectives(text) {
|
|
|
405
513
|
const kept = []
|
|
406
514
|
const rejected = []
|
|
407
515
|
for (const item of raw) {
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
516
|
+
// Both shapes are accepted: a bare sentence, or { text, workspace? }.
|
|
517
|
+
const source = typeof item === 'string' ? item : (item !== null && typeof item === 'object' && typeof item.text === 'string' ? item.text : null)
|
|
518
|
+
if (source === null) continue
|
|
519
|
+
const value = clipDirective(source)
|
|
520
|
+
const workspace = typeof item === 'object' && item !== null && typeof item.workspace === 'string' && item.workspace.trim().length > 0
|
|
521
|
+
? clipSafe(item.workspace.trim(), 200)
|
|
522
|
+
: undefined
|
|
523
|
+
const key = (workspace ?? '') + '\n' + value.toLowerCase()
|
|
411
524
|
if (value.length === 0 || seen.has(key)) continue
|
|
412
525
|
seen.add(key)
|
|
413
526
|
if (ASKS_USER_RE.test(value)) {
|
|
414
527
|
rejected.push(value)
|
|
415
528
|
continue
|
|
416
529
|
}
|
|
417
|
-
kept.push(value)
|
|
530
|
+
kept.push(workspace === undefined ? { text: value } : { text: value, workspace })
|
|
418
531
|
if (kept.length >= 4) break
|
|
419
532
|
}
|
|
420
533
|
return { kept, rejected }
|
|
421
534
|
}
|
|
422
535
|
|
|
423
536
|
/**
|
|
424
|
-
* The system-prompt section: what the agent is told about this user
|
|
425
|
-
*
|
|
537
|
+
* The system-prompt section: what the agent is told about this user, plus the
|
|
538
|
+
* ids of the directives that actually made it into the text (the directive
|
|
539
|
+
* cap and the character budget can drop enabled ones). `text` is '' when
|
|
540
|
+
* nothing is enabled (an empty section contributes nothing).
|
|
426
541
|
*/
|
|
427
|
-
export function
|
|
428
|
-
const
|
|
542
|
+
export function buildSteeringSection(profile, { cwd } = {}) {
|
|
543
|
+
const here = typeof cwd === 'string' && cwd.length > 0 ? cwd : null
|
|
544
|
+
const scopeOf = (entry) => (typeof entry.workspace === 'string' && entry.workspace.length > 0 ? entry.workspace : null)
|
|
545
|
+
const candidates = (Array.isArray(profile?.directives) ? profile.directives : [])
|
|
429
546
|
.filter((entry) => entry !== null && typeof entry === 'object' && entry.enabled !== false && entry.status !== 'retired'
|
|
430
547
|
&& typeof entry.text === 'string' && entry.text.trim().length > 0)
|
|
431
|
-
|
|
548
|
+
.filter((entry) => scopeOf(entry) === null || scopeOf(entry) === here)
|
|
549
|
+
// This workspace's own directives first: they are the more specific ones.
|
|
550
|
+
const enabled = [...candidates.filter((entry) => scopeOf(entry) !== null), ...candidates.filter((entry) => scopeOf(entry) === null)]
|
|
551
|
+
if (enabled.length === 0) return { text: '', ids: [] }
|
|
432
552
|
const header = [
|
|
433
553
|
'## About this user (learned by Tacit from their past prompts)',
|
|
434
554
|
'This user tends to leave the following unsaid. Compensate silently when the',
|
|
@@ -436,14 +556,21 @@ export function renderSteeringSection(profile) {
|
|
|
436
556
|
'the prompt always win over these notes.',
|
|
437
557
|
]
|
|
438
558
|
const lines = [...header]
|
|
559
|
+
const ids = []
|
|
439
560
|
let length = lines.join('\n').length
|
|
440
561
|
for (const entry of enabled.slice(0, MAX_DIRECTIVES)) {
|
|
441
|
-
const line = '- ' +
|
|
562
|
+
const line = '- ' + clipDirective(entry.text)
|
|
442
563
|
if (length + line.length + 1 > STEERING_MAX_CHARS) break
|
|
443
564
|
lines.push(line)
|
|
565
|
+
if (typeof entry.id === 'string') ids.push(entry.id)
|
|
444
566
|
length += line.length + 1
|
|
445
567
|
}
|
|
446
|
-
return lines.length > header.length ? lines.join('\n') : ''
|
|
568
|
+
return lines.length > header.length ? { text: lines.join('\n'), ids } : { text: '', ids: [] }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** The steering section text alone. */
|
|
572
|
+
export function renderSteeringSection(profile, options = {}) {
|
|
573
|
+
return buildSteeringSection(profile, options).text
|
|
447
574
|
}
|
|
448
575
|
|
|
449
576
|
/** Build the distillation user text from verbatim rejected-improvement reasons. */
|
|
@@ -653,6 +780,27 @@ const clipText = (value, max) => {
|
|
|
653
780
|
}
|
|
654
781
|
|
|
655
782
|
/** Shape a parsed analysis object into a report (falls back gracefully). */
|
|
783
|
+
/** Shape a good-prompt call into a report: no problems, the original prompt kept, the lesson as explanation. */
|
|
784
|
+
export function normalizeGoodReport(parsed, { turn, time, model, prompt }) {
|
|
785
|
+
const raw = parsed !== null && typeof parsed === 'object' ? parsed : {}
|
|
786
|
+
const strengths = (Array.isArray(raw.strengths) ? raw.strengths : [])
|
|
787
|
+
.filter((item) => item !== null && typeof item === 'object' && typeof item.what === 'string' && item.what.trim().length > 0)
|
|
788
|
+
.slice(0, 4)
|
|
789
|
+
.map((item) => ({ kind: clipText(typeof item.kind === 'string' ? item.kind : 'general', 60) || 'general', what: clipText(item.what, 600) }))
|
|
790
|
+
const lesson = typeof raw.lesson === 'string' ? clipText(raw.lesson.trim(), 300) : ''
|
|
791
|
+
return reportSchema.parse({
|
|
792
|
+
ok: true,
|
|
793
|
+
turn,
|
|
794
|
+
time,
|
|
795
|
+
model,
|
|
796
|
+
problems: [],
|
|
797
|
+
improvedPrompt: typeof prompt === 'string' ? clipText(prompt, 4000) : '',
|
|
798
|
+
explanation: lesson,
|
|
799
|
+
strengths,
|
|
800
|
+
lesson,
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
|
|
656
804
|
export function normalizeReport(parsed, { turn, time, model, rawText }) {
|
|
657
805
|
if (parsed === null) {
|
|
658
806
|
return {
|
|
@@ -729,9 +877,16 @@ export function aggregateProfile(prev, report, maxPatterns, options = {}) {
|
|
|
729
877
|
rejected: counterOf(pattern, 'rejected'),
|
|
730
878
|
verified: counterOf(pattern, 'verified'),
|
|
731
879
|
unverified: counterOf(pattern, 'unverified'),
|
|
880
|
+
resolved: counterOf(pattern, 'resolved'),
|
|
732
881
|
})
|
|
733
882
|
}
|
|
734
883
|
}
|
|
884
|
+
// A good-prompt report says which habits the user overcame on their own this time.
|
|
885
|
+
for (const strength of Array.isArray(report?.strengths) ? report.strengths : []) {
|
|
886
|
+
if (strength === null || typeof strength !== 'object') continue
|
|
887
|
+
const current = patterns.get(normalizeKind(strength.kind))
|
|
888
|
+
if (current !== undefined) current.resolved += 1
|
|
889
|
+
}
|
|
735
890
|
const problems = Array.isArray(report?.problems) ? report.problems : []
|
|
736
891
|
for (const problem of problems) {
|
|
737
892
|
if (problem === null || typeof problem !== 'object') continue
|
|
@@ -745,6 +900,7 @@ export function aggregateProfile(prev, report, maxPatterns, options = {}) {
|
|
|
745
900
|
rejected: 0,
|
|
746
901
|
verified: 0,
|
|
747
902
|
unverified: 0,
|
|
903
|
+
resolved: 0,
|
|
748
904
|
}
|
|
749
905
|
current.count += 1
|
|
750
906
|
if (typeof problem.what === 'string' && problem.what.length > 0) current.lastExample = problem.what.slice(0, 200)
|
package/lib/index.js
CHANGED
|
@@ -80,6 +80,19 @@ export function apply(ctx, config) {
|
|
|
80
80
|
if (typeof off === 'function') ctx.effect(() => off, 'tacit: pre-step enrichment')
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// One line so an audit can tell from the logs alone that Tacit is loaded
|
|
84
|
+
// and what it is currently injecting.
|
|
85
|
+
try {
|
|
86
|
+
const cfg = effectiveConfig()
|
|
87
|
+
const directives = store.profile().directives
|
|
88
|
+
const count = (status) => directives.filter((entry) => entry.status === status).length
|
|
89
|
+
console.info('[tacit] loaded — directives: ' + count('active') + ' active, ' + count('candidate') + ' candidates, '
|
|
90
|
+
+ count('retired') + ' retired; steering ' + (cfg.steerAgent ? 'on' : 'off') + '; auto-analysis '
|
|
91
|
+
+ (cfg.autoAnalyze ? 'on (cap ' + cfg.autoDailyBudget + '/day)' : 'off'))
|
|
92
|
+
} catch {
|
|
93
|
+
// Logging must never keep the plugin from loading.
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
ctx.effect(() => () => {
|
|
84
97
|
// Nothing to flush: all writes are atomic and complete at call time.
|
|
85
98
|
}, 'tacit: dispose')
|
package/lib/routes.js
CHANGED
|
@@ -126,7 +126,7 @@ export function registerWebRoutes(ctx, service) {
|
|
|
126
126
|
}))
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
route('POST', '/api/tacit/state', () => service.getState())
|
|
129
|
+
route('POST', '/api/tacit/state', (body) => service.getState(body))
|
|
130
130
|
route('POST', '/api/tacit/reports', (body) => service.getReports(body))
|
|
131
131
|
route('POST', '/api/tacit/history', (body) => service.listHistory(body))
|
|
132
132
|
route('POST', '/api/tacit/analyze', (body) => service.analyzeTurn(body))
|
package/lib/schema.js
CHANGED
|
@@ -59,6 +59,10 @@ export const Config = z.preprocess((v) => v ?? {}, z.object({
|
|
|
59
59
|
directiveTrialTurns: z.number().default(10),
|
|
60
60
|
/** A candidate retires when the messy-turn rate during its trial exceeds the baseline by more than this. */
|
|
61
61
|
directiveWorseBy: z.number().default(0.15),
|
|
62
|
+
/** Bootstrap analyses run at once (1 = one after another; same calls, less waiting). */
|
|
63
|
+
bootstrapConcurrency: z.number().default(1),
|
|
64
|
+
/** Also learn from a clean turn that follows a messy one (what the user included the second time). Automatic, capped. */
|
|
65
|
+
learnFromGood: z.boolean().default(true),
|
|
62
66
|
}))
|
|
63
67
|
|
|
64
68
|
/**
|
|
@@ -82,6 +86,8 @@ const configPatchSchema = z.object({
|
|
|
82
86
|
enrichPrompts: z.boolean().optional(),
|
|
83
87
|
directiveTrialTurns: z.number().optional(),
|
|
84
88
|
directiveWorseBy: z.number().optional(),
|
|
89
|
+
bootstrapConcurrency: z.number().optional(),
|
|
90
|
+
learnFromGood: z.boolean().optional(),
|
|
85
91
|
})
|
|
86
92
|
|
|
87
93
|
// ── Trajectory projection ──────────────────────────────────────────────────
|
|
@@ -160,6 +166,12 @@ export const reportSchema = z.object({
|
|
|
160
166
|
trigger: z.string().default('manual'),
|
|
161
167
|
/** The user's next message when it triggered the analysis (clipped). */
|
|
162
168
|
followUp: z.string().optional(),
|
|
169
|
+
/** Absolute workspace directory of the conversation, when the harness knew it. */
|
|
170
|
+
cwd: z.string().optional(),
|
|
171
|
+
/** trigger 'good' only: what the clean prompt supplied that the messy one before it lacked. */
|
|
172
|
+
strengths: z.array(z.object({ kind: z.string(), what: z.string() })).optional(),
|
|
173
|
+
/** trigger 'good' only: the one-sentence lesson fed to the distiller. */
|
|
174
|
+
lesson: z.string().optional(),
|
|
163
175
|
})
|
|
164
176
|
|
|
165
177
|
/**
|
|
@@ -178,6 +190,8 @@ export const patternCountersSchema = z.object({
|
|
|
178
190
|
verified: z.number().int().default(0),
|
|
179
191
|
/** Times the next turn's outcome was same/worse than the baseline. */
|
|
180
192
|
unverified: z.number().int().default(0),
|
|
193
|
+
/** Times a clean prompt right after a messy turn showed the user supplying this themselves. */
|
|
194
|
+
resolved: z.number().int().default(0),
|
|
181
195
|
})
|
|
182
196
|
|
|
183
197
|
/** One distilled durable style rule (from rejected-improvement reasons). */
|
|
@@ -219,6 +233,8 @@ const directiveSchema = z.object({
|
|
|
219
233
|
status: z.enum(['candidate', 'active', 'retired']).default('active'),
|
|
220
234
|
trial: directiveTrialSchema.optional(),
|
|
221
235
|
retiredReason: z.string().optional(),
|
|
236
|
+
/** Absolute workspace directory this directive is limited to; absent = every conversation. */
|
|
237
|
+
workspace: z.string().optional(),
|
|
222
238
|
})
|
|
223
239
|
|
|
224
240
|
/** The persistent user-wide mistake profile. */
|
|
@@ -257,7 +273,7 @@ export const statsArgSchema = z.object({
|
|
|
257
273
|
|
|
258
274
|
export const directivesArgSchema = z.discriminatedUnion('action', [
|
|
259
275
|
z.object({ action: z.literal('toggle'), id: z.string().min(1).max(64), enabled: z.boolean() }),
|
|
260
|
-
z.object({ action: z.literal('add'), text: z.string().min(1).max(300) }),
|
|
276
|
+
z.object({ action: z.literal('add'), text: z.string().min(1).max(300), workspace: z.string().max(1000).optional() }),
|
|
261
277
|
z.object({ action: z.literal('remove'), id: z.string().min(1).max(64) }),
|
|
262
278
|
])
|
|
263
279
|
|