osborn 0.9.4 → 0.9.9
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/.claude/settings.local.json +9 -0
- package/.claude/skills/browser-apply/SKILL.md +114 -0
- package/.claude/skills/markdown-to-pdf/SKILL.md +29 -0
- package/.claude/skills/pdf-to-markdown/SKILL.md +28 -0
- package/.claude/skills/playwright-browser/SKILL.md +90 -0
- package/.claude/skills/shadcn/SKILL.md +232 -0
- package/.claude/skills/shadcn/image.png +0 -0
- package/.claude/skills/youtube-transcript/SKILL.md +24 -0
- package/caresource-apply.js +50 -0
- package/caresource-apply.mjs +34 -0
- package/dist/claude-llm.js +219 -37
- package/dist/conversation-brain.d.ts +92 -0
- package/dist/conversation-brain.js +360 -0
- package/dist/fast-llm.d.ts +15 -0
- package/dist/fast-llm.js +81 -0
- package/dist/index.js +288 -57
- package/dist/pipeline-direct-llm.js +1 -1
- package/dist/prompts/compact-learnings-instruction.md +53 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,11 +10,11 @@ initializeLogger({ pretty: true, level: 'info' });
|
|
|
10
10
|
import { setMaxListeners } from 'node:events';
|
|
11
11
|
setMaxListeners(50);
|
|
12
12
|
import { createServer } from 'http';
|
|
13
|
-
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync, cpSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync, cpSync, rmSync, renameSync, statSync, createWriteStream } from 'node:fs';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
-
import { homedir } from 'node:os';
|
|
17
|
+
import { homedir, tmpdir } from 'node:os';
|
|
18
18
|
// Resolve __dirname for this ESM module so we can find sibling files (e.g.
|
|
19
19
|
// meeting-output.html) relative to the compiled JS location, NOT process.cwd().
|
|
20
20
|
// In production cwd is the user's workspace; the static file lives next to dist/index.js.
|
|
@@ -331,6 +331,38 @@ function startApiServer(workingDir, port) {
|
|
|
331
331
|
res.destroy(); });
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
|
+
// GET /sessions/manifest — return mtime+size for all .jsonl files per slug (public, no auth)
|
|
335
|
+
if (req.method === 'GET' && url.pathname === '/sessions/manifest') {
|
|
336
|
+
const claudeDir = join(homedir(), '.claude', 'projects');
|
|
337
|
+
const slugMap = {};
|
|
338
|
+
try {
|
|
339
|
+
const slugs = readdirSync(claudeDir, { withFileTypes: true })
|
|
340
|
+
.filter(d => d.isDirectory())
|
|
341
|
+
.map(d => d.name);
|
|
342
|
+
for (const slug of slugs) {
|
|
343
|
+
const slugDir = join(claudeDir, slug);
|
|
344
|
+
try {
|
|
345
|
+
const jsonlFiles = readdirSync(slugDir)
|
|
346
|
+
.filter(f => f.endsWith('.jsonl'));
|
|
347
|
+
const fileStats = {};
|
|
348
|
+
for (const file of jsonlFiles) {
|
|
349
|
+
const st = statSync(join(slugDir, file));
|
|
350
|
+
fileStats[file] = { mtime: st.mtimeMs, size: st.size };
|
|
351
|
+
}
|
|
352
|
+
slugMap[slug] = { files: fileStats };
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// skip unreadable dirs
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// projects dir doesn't exist yet — return empty
|
|
361
|
+
}
|
|
362
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
363
|
+
res.end(JSON.stringify({ slugs: slugMap }));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
334
366
|
// POST /sessions/import — accept a gzipped tar and extract into ~/.claude/projects/
|
|
335
367
|
if (req.method === 'POST' && url.pathname === '/sessions/import') {
|
|
336
368
|
if (syncToken) {
|
|
@@ -342,83 +374,282 @@ function startApiServer(workingDir, port) {
|
|
|
342
374
|
}
|
|
343
375
|
}
|
|
344
376
|
const targetWorkDir = url.searchParams.get('targetWorkDir');
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
377
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'osborn-import-'));
|
|
378
|
+
const tarProc = spawn('tar', ['-xf', '-', '-C', tmpDir]);
|
|
379
|
+
// Streaming: pipe request body directly to tar stdin — no buffering
|
|
380
|
+
req.pipe(tarProc.stdin);
|
|
381
|
+
tarProc.stdin.on('error', (err) => {
|
|
382
|
+
console.error('[import] tar stdin error', err);
|
|
383
|
+
tarProc.kill('SIGTERM');
|
|
384
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
385
|
+
if (!res.headersSent) {
|
|
386
|
+
res.writeHead(500);
|
|
387
|
+
res.end(JSON.stringify({ error: 'upload error' }));
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
req.on('aborted', () => {
|
|
391
|
+
tarProc.kill('SIGTERM');
|
|
392
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
393
|
+
});
|
|
394
|
+
tarProc.stderr.on('data', (d) => console.error('[import]', d.toString()));
|
|
395
|
+
tarProc.on('close', async (code) => {
|
|
350
396
|
try {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
397
|
+
if (code !== 0) {
|
|
398
|
+
res.writeHead(500);
|
|
399
|
+
res.end(JSON.stringify({ error: 'tar extraction failed', code }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const claudeDir = join(homedir(), '.claude');
|
|
403
|
+
const projectsDir = join(claudeDir, 'projects');
|
|
404
|
+
mkdirSync(projectsDir, { recursive: true });
|
|
405
|
+
// The archive should contain a 'projects' subdirectory
|
|
406
|
+
const extractedProjects = join(tmpDir, 'projects');
|
|
407
|
+
const sourceDir = existsSync(extractedProjects) ? extractedProjects : tmpDir;
|
|
408
|
+
// Optionally remap slug: if targetWorkDir is provided, find slug(s)
|
|
409
|
+
// that don't match the target and rename them
|
|
410
|
+
const remapped = {};
|
|
411
|
+
if (targetWorkDir) {
|
|
412
|
+
const targetSlug = targetWorkDir.replace(/\//g, '-');
|
|
413
|
+
const sourceSlugs = readdirSync(sourceDir);
|
|
414
|
+
for (const slug of sourceSlugs) {
|
|
415
|
+
if (slug !== targetSlug && !slug.startsWith('.')) {
|
|
416
|
+
remapped[slug] = targetSlug;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Copy subdirectories into ~/.claude/projects/, merging and updating existing files
|
|
421
|
+
let filesWritten = 0;
|
|
422
|
+
const slugsInSource = readdirSync(sourceDir);
|
|
423
|
+
for (const slug of slugsInSource) {
|
|
424
|
+
const effectiveSlug = remapped[slug] ?? slug;
|
|
425
|
+
const destSlug = join(projectsDir, effectiveSlug);
|
|
426
|
+
mkdirSync(destSlug, { recursive: true });
|
|
427
|
+
try {
|
|
428
|
+
renameSync(join(sourceDir, slug), destSlug);
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
if (e.code === 'EXDEV') {
|
|
432
|
+
// Cross-filesystem fallback
|
|
433
|
+
cpSync(join(sourceDir, slug), destSlug, { recursive: true, force: true, errorOnExist: false });
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
throw e;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
filesWritten++;
|
|
440
|
+
}
|
|
441
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
442
|
+
res.end(JSON.stringify({ ok: true, filesWritten, remapped }));
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
console.error('[import] merge error:', err);
|
|
446
|
+
if (!res.headersSent) {
|
|
447
|
+
res.writeHead(500);
|
|
448
|
+
res.end(JSON.stringify({ error: 'Failed to merge sessions', detail: String(err) }));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// POST /sessions/import-chunk — accept a single chunk of a multi-part upload
|
|
458
|
+
if (req.method === 'POST' && url.pathname === '/sessions/import-chunk') {
|
|
459
|
+
if (syncToken) {
|
|
460
|
+
const authHeader = req.headers['authorization'] ?? '';
|
|
461
|
+
if (authHeader !== `Bearer ${syncToken}`) {
|
|
462
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
463
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const uploadId = url.searchParams.get('uploadId');
|
|
468
|
+
const chunkIndex = parseInt(url.searchParams.get('chunk') || '0');
|
|
469
|
+
if (!uploadId) {
|
|
470
|
+
res.writeHead(400);
|
|
471
|
+
res.end(JSON.stringify({ error: 'uploadId required' }));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
// Chunk storage dir: /tmp/osborn-upload-<uploadId>/
|
|
475
|
+
const uploadDir = join(tmpdir(), `osborn-upload-${uploadId}`);
|
|
476
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
477
|
+
// Write chunk to padded filename for correct sort order
|
|
478
|
+
const chunkPath = join(uploadDir, `chunk-${String(chunkIndex).padStart(6, '0')}`);
|
|
479
|
+
const writeStream = createWriteStream(chunkPath);
|
|
480
|
+
req.pipe(writeStream);
|
|
481
|
+
writeStream.on('finish', () => {
|
|
482
|
+
res.writeHead(200);
|
|
483
|
+
res.end(JSON.stringify({ ok: true, chunk: chunkIndex }));
|
|
484
|
+
});
|
|
485
|
+
writeStream.on('error', (err) => {
|
|
486
|
+
console.error('[import-chunk] write error', err);
|
|
487
|
+
if (!res.headersSent) {
|
|
488
|
+
res.writeHead(500);
|
|
489
|
+
res.end(JSON.stringify({ error: 'write failed' }));
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
req.on('aborted', () => { writeStream.destroy(); });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// POST /sessions/import-finalize — reassemble chunks, extract, apply slug merge
|
|
496
|
+
if (req.method === 'POST' && url.pathname === '/sessions/import-finalize') {
|
|
497
|
+
if (syncToken) {
|
|
498
|
+
const authHeader = req.headers['authorization'] ?? '';
|
|
499
|
+
if (authHeader !== `Bearer ${syncToken}`) {
|
|
500
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
501
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const uploadId = url.searchParams.get('uploadId');
|
|
506
|
+
const total = parseInt(url.searchParams.get('total') || '0');
|
|
507
|
+
const targetWorkDir = url.searchParams.get('targetWorkDir') ?? undefined;
|
|
508
|
+
if (!uploadId || total === 0) {
|
|
509
|
+
res.writeHead(400);
|
|
510
|
+
res.end(JSON.stringify({ error: 'uploadId and total required' }));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const uploadDir = join(tmpdir(), `osborn-upload-${uploadId}`);
|
|
514
|
+
// Verify all chunks present
|
|
515
|
+
const expectedChunks = Array.from({ length: total }, (_, i) => `chunk-${String(i).padStart(6, '0')}`);
|
|
516
|
+
const presentChunks = existsSync(uploadDir) ? readdirSync(uploadDir).filter(f => f.startsWith('chunk-')).sort() : [];
|
|
517
|
+
const missing = expectedChunks.filter(c => !presentChunks.includes(c));
|
|
518
|
+
if (missing.length > 0) {
|
|
519
|
+
res.writeHead(400);
|
|
520
|
+
res.end(JSON.stringify({ error: 'missing chunks', missing }));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const tmpExtractDir = mkdtempSync(join(tmpdir(), 'osborn-import-'));
|
|
524
|
+
try {
|
|
525
|
+
// Reassemble chunks into a single stream and pipe to tar
|
|
526
|
+
const tarProc = spawn('tar', ['-xf', '-', '-C', tmpExtractDir]);
|
|
527
|
+
// Stream chunks in order to tar stdin
|
|
528
|
+
const streamChunks = async () => {
|
|
529
|
+
for (const chunkFile of expectedChunks) {
|
|
530
|
+
const chunkData = readFileSync(join(uploadDir, chunkFile));
|
|
531
|
+
await new Promise((resolve, reject) => {
|
|
532
|
+
tarProc.stdin.write(chunkData, (err) => {
|
|
533
|
+
if (err)
|
|
534
|
+
reject(err);
|
|
535
|
+
else
|
|
536
|
+
resolve();
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
}
|
|
354
540
|
tarProc.stdin.end();
|
|
355
|
-
|
|
356
|
-
|
|
541
|
+
};
|
|
542
|
+
streamChunks().catch(err => {
|
|
543
|
+
console.error('[import-finalize] chunk stream error', err);
|
|
544
|
+
tarProc.kill('SIGTERM');
|
|
545
|
+
});
|
|
546
|
+
tarProc.stderr.on('data', (d) => console.error('[import-finalize]', d.toString()));
|
|
547
|
+
tarProc.on('close', async (code) => {
|
|
548
|
+
try {
|
|
357
549
|
if (code !== 0) {
|
|
358
|
-
res.writeHead(500
|
|
550
|
+
res.writeHead(500);
|
|
359
551
|
res.end(JSON.stringify({ error: 'tar extraction failed', code }));
|
|
360
552
|
return;
|
|
361
553
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const newSlug = targetSlug;
|
|
378
|
-
remapped[slug] = newSlug;
|
|
379
|
-
}
|
|
554
|
+
const claudeDir = join(homedir(), '.claude');
|
|
555
|
+
const projectsDir = join(claudeDir, 'projects');
|
|
556
|
+
mkdirSync(projectsDir, { recursive: true });
|
|
557
|
+
// The archive should contain a 'projects' subdirectory
|
|
558
|
+
const extractedProjects = join(tmpExtractDir, 'projects');
|
|
559
|
+
const sourceDir = existsSync(extractedProjects) ? extractedProjects : tmpExtractDir;
|
|
560
|
+
// Optionally remap slug: if targetWorkDir is provided, find slug(s)
|
|
561
|
+
// that don't match the target and rename them
|
|
562
|
+
const remapped = {};
|
|
563
|
+
if (targetWorkDir) {
|
|
564
|
+
const targetSlug = targetWorkDir.replace(/\//g, '-');
|
|
565
|
+
const sourceSlugs = readdirSync(sourceDir);
|
|
566
|
+
for (const slug of sourceSlugs) {
|
|
567
|
+
if (slug !== targetSlug && !slug.startsWith('.')) {
|
|
568
|
+
remapped[slug] = targetSlug;
|
|
380
569
|
}
|
|
381
570
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
errorOnExist: false,
|
|
393
|
-
});
|
|
394
|
-
filesWritten++;
|
|
571
|
+
}
|
|
572
|
+
// Copy subdirectories into ~/.claude/projects/, merging and updating existing files
|
|
573
|
+
let filesWritten = 0;
|
|
574
|
+
const slugsInSource = readdirSync(sourceDir);
|
|
575
|
+
for (const slug of slugsInSource) {
|
|
576
|
+
const effectiveSlug = remapped[slug] ?? slug;
|
|
577
|
+
const destSlug = join(projectsDir, effectiveSlug);
|
|
578
|
+
mkdirSync(destSlug, { recursive: true });
|
|
579
|
+
try {
|
|
580
|
+
renameSync(join(sourceDir, slug), destSlug);
|
|
395
581
|
}
|
|
396
|
-
|
|
397
|
-
|
|
582
|
+
catch (e) {
|
|
583
|
+
if (e.code === 'EXDEV') {
|
|
584
|
+
// Cross-filesystem fallback
|
|
585
|
+
cpSync(join(sourceDir, slug), destSlug, { recursive: true, force: true, errorOnExist: false });
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
throw e;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
filesWritten++;
|
|
398
592
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
593
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
594
|
+
res.end(JSON.stringify({ ok: true, filesWritten, remapped }));
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
console.error('[import-finalize] merge error:', err);
|
|
598
|
+
if (!res.headersSent) {
|
|
599
|
+
res.writeHead(500);
|
|
402
600
|
res.end(JSON.stringify({ error: 'Failed to merge sessions', detail: String(err) }));
|
|
403
601
|
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
rmSync(uploadDir, { recursive: true, force: true });
|
|
605
|
+
rmSync(tmpExtractDir, { recursive: true, force: true });
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
rmSync(uploadDir, { recursive: true, force: true });
|
|
611
|
+
rmSync(tmpExtractDir, { recursive: true, force: true });
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
412
614
|
return;
|
|
413
615
|
}
|
|
414
616
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
415
617
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
416
618
|
});
|
|
417
619
|
const host = process.env.HOST || '0.0.0.0';
|
|
620
|
+
server.requestTimeout = 0; // no timeout — large uploads can take minutes
|
|
418
621
|
server.listen(port, host, () => {
|
|
419
622
|
console.log(`🌐 API server listening on http://${host}:${port}`);
|
|
420
623
|
console.log(` Sessions: http://${host}:${port}/sessions`);
|
|
421
624
|
});
|
|
625
|
+
// Stale upload-chunk cleanup: remove osborn-upload-* dirs older than 30 minutes
|
|
626
|
+
const cleanStaleUploadDirs = () => {
|
|
627
|
+
const tmp = tmpdir();
|
|
628
|
+
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
629
|
+
try {
|
|
630
|
+
const entries = readdirSync(tmp);
|
|
631
|
+
for (const entry of entries) {
|
|
632
|
+
if (!entry.startsWith('osborn-upload-'))
|
|
633
|
+
continue;
|
|
634
|
+
const full = `${tmp}/${entry}`;
|
|
635
|
+
try {
|
|
636
|
+
const st = statSync(full);
|
|
637
|
+
if (st.isDirectory() && st.mtimeMs < cutoff) {
|
|
638
|
+
rmSync(full, { recursive: true, force: true });
|
|
639
|
+
console.log(`🧹 Removed stale upload dir: ${full}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
// ignore per-entry errors
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// ignore if /tmp is unreadable
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
cleanStaleUploadDirs();
|
|
652
|
+
setInterval(cleanStaleUploadDirs, 10 * 60 * 1000);
|
|
422
653
|
server.on('error', (err) => {
|
|
423
654
|
if (err.code === 'EADDRINUSE') {
|
|
424
655
|
console.warn(`⚠️ API port ${port} in use, trying ${port + 1}...`);
|
|
@@ -106,7 +106,7 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
106
106
|
`- If it's a quick side question, answer it then continue where you left off`,
|
|
107
107
|
`- If they want to change direction, acknowledge and follow their lead`,
|
|
108
108
|
`- Clarify when asked to or the question requires going over what you just said`,
|
|
109
|
-
`-
|
|
109
|
+
`- If relevant details were cut off — whether they answer the current question or an earlier one — weave them back in naturally so the user stays in context without having to ask again.`,
|
|
110
110
|
].join('\n');
|
|
111
111
|
// Modify the last user message in chatCtx
|
|
112
112
|
for (let i = chatCtx.items.length - 1; i >= 0; i--) {
|
|
@@ -1,14 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
# Pre-Compaction Instructions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Before Claude Code compacts this conversation, you MUST preserve critical context by including the following four sections at the very END of your compact summary. Be selective and specific — vague summaries are useless.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
1. USER CORRECTIONS — things the user explicitly told you to stop/start doing (e.g., "stop patching scripts", "use step-by-step not autonomous", "don't name sub-agents")
|
|
7
|
-
2. USER PREFERENCES — recurring patterns in how the user works (e.g., "prefers yahoo email for new accounts", "wants visible browser not headless", "expects score-based salary calculation")
|
|
8
|
-
3. DOMAIN KNOWLEDGE — specific technical facts verified during this session (e.g., "Workday sign-in is inline switch not modal", "CareSource hibernation is 30s confirmed")
|
|
9
|
-
4. EFFECTIVE PATTERNS — approaches that worked and the user confirmed (e.g., "co-direction questions during steerable work", "grounding with 2 reads before speaking")
|
|
10
|
-
5. ANTI-PATTERNS — approaches that failed or frustrated the user (e.g., "reporting exit code 0 as success without verification", "multiple browser windows from competing automation approaches")
|
|
5
|
+
---
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
## Section 1: HANDOFF_STATE
|
|
8
|
+
Format: === HANDOFF_STATE ===
|
|
9
|
+
Include:
|
|
10
|
+
- **Current goal**: What are we building/fixing and WHY (the big-picture reason, not just the immediate task)
|
|
11
|
+
- **Progress**: What is done, what is in-progress, what is the very next step
|
|
12
|
+
- **Active facts**: Environment details mentioned in this session (API keys referenced, service URLs, version numbers confirmed, file paths that matter)
|
|
13
|
+
- **Test results**: What was tried, what it showed, what it ruled out
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Keep this under 800 characters. This stays in the compact summary — it is NOT written to disk.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Section 2: DECISIONS
|
|
20
|
+
Format: === DECISIONS ===
|
|
21
|
+
List each architectural or project decision made in this session, one per line:
|
|
22
|
+
- DECISION: <choice made> | RATIONALE: <why> | SCOPE: project
|
|
23
|
+
|
|
24
|
+
Only include decisions that would matter in a future session on the same project. Skip trivial choices.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Section 3: SKILL_CANDIDATES
|
|
29
|
+
Format: === SKILL_CANDIDATES ===
|
|
30
|
+
For each reusable how-to procedure confirmed working in this session, emit:
|
|
31
|
+
|
|
32
|
+
--- SKILL: <kebab-case-name> ---
|
|
33
|
+
WHEN: <one line: when this skill applies>
|
|
34
|
+
STEPS:
|
|
35
|
+
1. ...
|
|
36
|
+
2. ...
|
|
37
|
+
VERIFIED: <exact command or observation that confirmed it works>
|
|
38
|
+
--- END SKILL ---
|
|
39
|
+
|
|
40
|
+
A skill is worth extracting only if: (1) it was confirmed working in this session AND (2) it would apply to future sessions on different tasks. Do NOT re-emit skills already shown in the EXISTING SKILLS section unless they need substantive updates.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Section 4: BEHAVIORAL_LEARNINGS
|
|
45
|
+
Format: === BEHAVIORAL_LEARNINGS ===
|
|
46
|
+
Capture user corrections, preferences, and anti-patterns in these subsections:
|
|
47
|
+
USER CORRECTIONS:
|
|
48
|
+
USER PREFERENCES:
|
|
49
|
+
DOMAIN KNOWLEDGE:
|
|
50
|
+
EFFECTIVE PATTERNS:
|
|
51
|
+
ANTI-PATTERNS:
|
|
52
|
+
|
|
53
|
+
If the EXISTING LEARNED SKILLS section is shown below, merge — update outdated items, add new ones, keep confirmed ones.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
IMPORTANT: All four sections MUST appear at the end of the compact summary even if some are empty. Empty sections should have a single line: (none this session)
|