thoth-plugin 1.1.0 → 1.1.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/dist/index.js CHANGED
@@ -405,37 +405,211 @@ Before stating any fact about Zeus's life:
405
405
  - *If NOT FOUND*: Say "I don't have that recorded" \u2014 never guess
406
406
  </Knowledge_Management>`;
407
407
  // src/specialization/prompt-builder.ts
408
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
408
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync } from "fs";
409
+ import { join as join3 } from "path";
410
+ import { homedir } from "os";
411
+ function parseSkillTriggers(content) {
412
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
413
+ const match = content.match(frontmatterRegex);
414
+ if (!match)
415
+ return null;
416
+ const yaml = match[1];
417
+ let name = "";
418
+ const triggers = [];
419
+ let inTriggers = false;
420
+ for (const line of yaml.split(`
421
+ `)) {
422
+ const trimmed = line.trim();
423
+ if (trimmed.startsWith("name:")) {
424
+ name = trimmed.slice(5).trim();
425
+ inTriggers = false;
426
+ } else if (trimmed === "triggers:") {
427
+ inTriggers = true;
428
+ } else if (inTriggers && trimmed.startsWith("- ")) {
429
+ triggers.push(trimmed.slice(2).trim());
430
+ } else if (trimmed.includes(":") && !trimmed.startsWith("-")) {
431
+ inTriggers = false;
432
+ }
433
+ }
434
+ return triggers.length > 0 ? { name, triggers } : null;
435
+ }
436
+ function discoverTriggersFromDir(skillsDir) {
437
+ if (!existsSync3(skillsDir))
438
+ return [];
439
+ const results = [];
440
+ try {
441
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
442
+ for (const entry of entries) {
443
+ if (entry.name.startsWith("."))
444
+ continue;
445
+ if (!entry.isDirectory())
446
+ continue;
447
+ const skillMdPath = join3(skillsDir, entry.name, "SKILL.md");
448
+ if (!existsSync3(skillMdPath))
449
+ continue;
450
+ try {
451
+ const content = readFileSync2(skillMdPath, "utf-8");
452
+ const parsed = parseSkillTriggers(content);
453
+ if (parsed) {
454
+ results.push(parsed);
455
+ }
456
+ } catch {}
457
+ }
458
+ } catch {}
459
+ return results;
460
+ }
461
+ function buildSkillRoutingSection() {
462
+ const projectSkillsDir = join3(process.cwd(), ".opencode", "skill");
463
+ const userSkillsDir = join3(homedir(), ".opencode", "skill");
464
+ const projectTriggers = discoverTriggersFromDir(projectSkillsDir);
465
+ const userTriggers = discoverTriggersFromDir(userSkillsDir);
466
+ const seen = new Set;
467
+ const allTriggers = [];
468
+ for (const t of [...projectTriggers, ...userTriggers]) {
469
+ if (!seen.has(t.name)) {
470
+ seen.add(t.name);
471
+ allTriggers.push(t);
472
+ }
473
+ }
474
+ if (allTriggers.length === 0) {
475
+ return "";
476
+ }
477
+ const lines = [
478
+ "<Skill_Routing>",
479
+ "## Skill Routing (CHECK BEFORE RESPONDING)",
480
+ "",
481
+ "Before responding to user requests, check if their intent matches a skill trigger:",
482
+ ""
483
+ ];
484
+ for (const skill of allTriggers) {
485
+ const triggers = skill.triggers.map((t) => `"${t}"`).join(", ");
486
+ lines.push(`- ${triggers} \u2192 \`skill({ skill: "${skill.name}" })\``);
487
+ }
488
+ lines.push("");
489
+ lines.push("**Rule**: If user intent matches a trigger, invoke the skill immediately. Don't improvise workflows that already exist.");
490
+ lines.push("</Skill_Routing>");
491
+ return lines.join(`
492
+ `);
493
+ }
494
+ var THOTH_INTENT_GATE = `<Phase_0_Intent_Gate>
495
+ ## Phase 0: Intent Gate (EVERY prompt)
496
+
497
+ Before ANY action, classify the incoming request:
498
+
499
+ ### Step 0: Check for Skills
500
+ | Trigger | Skill | Action |
501
+ |---------|-------|--------|
502
+ | "Run morning boot", "Start my day" | morning-boot | Fire skill immediately |
503
+ | "End of day", "Close out" | evening-close | Fire skill immediately |
504
+ | "Dump:", "Quick thought:" | thought-router | Fire skill immediately |
505
+ | "Drill meeting notes" | post-meeting-drill | Fire skill immediately |
506
+
507
+ ### Step 1: Identify Hemisphere(s)
508
+ | Signal | Hemisphere |
509
+ |--------|------------|
510
+ | Code, technical, development, bugs, features, git | **coding/** |
511
+ | Work, job, colleagues, projects, meetings, career, stakeholders | **work/** |
512
+ | Personal, health, family, friends, home, finance, feelings | **life/** |
513
+ | System, settings, preferences, onboarding, meta, Thoth | **kernel/** |
514
+ | Ambiguous or cross-domain | Multiple or ask |
515
+
516
+ ### Step 2: Check Permissions
517
+ Before any action, verify against kernel/config/permissions.md:
518
+ - **Autonomous**: Read, analyze, create knowledge, fire background agents
519
+ - **Requires Approval**: Send communications, financial, delete, modify shared files
520
+
521
+ ### Step 3: Check Trust Level
522
+ Read kernel/state/trust.md for current trust level:
523
+ - **Level 1**: Read only, all writes require approval
524
+ - **Level 2**: Code edits with evidence, knowledge updates
525
+ - **Level 3**: Routine communications, calendar changes
526
+
527
+ ### Step 4: Classify Request Type
528
+ | Type | Signal | Action |
529
+ |------|--------|--------|
530
+ | **Information** | "What is...", "Who is...", "Tell me about..." | Retrieve context \u2192 answer |
531
+ | **Action** | "Send...", "Schedule...", "Create...", "Update..." | Check permissions \u2192 execute or delegate |
532
+ | **Planning** | "Help me plan...", "How should I..." | Retrieve context \u2192 think \u2192 propose |
533
+ | **Coaching** | "Reflect", "How am I doing" | Thoughtful dialogue |
534
+ | **Onboarding** | "Let's onboard...", "Learn about my..." | Enter onboarding mode |
535
+ | **Meta** | "Update your prompt", "Change how you..." | Collaborative self-modification |
536
+
537
+ ### Step 5: Determine Routing
538
+ | Complexity | Action |
539
+ |------------|--------|
540
+ | Simple, single-hemisphere | Handle directly with context from that hemisphere |
541
+ | Complex, single-hemisphere | Delegate to hemisphere Master |
542
+ | Cross-hemisphere | Orchestrate: gather context from multiple, synthesize |
543
+ | Requires research | Fire parallel background_task agents |
544
+ </Phase_0_Intent_Gate>`;
409
545
  var THOTH_CORE_CAPABILITIES = `<Core_Capabilities>
410
- ## Core Capabilities
546
+ ## Core Capabilities (Built-In)
411
547
 
412
- ### When to Delegate vs Execute Directly
548
+ You have these capabilities built into your core function. Do NOT delegate these to sub-agents \u2014 execute them directly with full session context.
413
549
 
414
- | Situation | Action |
415
- |-----------|--------|
416
- | Simple lookup, single file read | Execute directly |
417
- | Knowledge base update | Execute directly |
418
- | Parallel research (multiple sources) | Fire background agents |
419
- | Complex workflow with defined steps | Invoke a skill |
420
- | Deep domain work requiring focus | Delegate to sub-agent |
550
+ ### Knowledge Retrieval (You Do This Directly)
421
551
 
422
- ### Background Agents
552
+ When Zeus asks "What do I know about X?", "Who is Y?", "Context on Z?":
423
553
 
424
- For parallel research or data gathering. They run independently and return results:
554
+ 1. **Read \`kernel/paths.json\`** to locate files
555
+ 2. **Follow Circle System** (see Phase 1 below)
556
+ 3. **Synthesize** the relevant information
557
+ 4. **Cite sources** \u2014 always reference which files you found information in
558
+ 5. **Acknowledge gaps** \u2014 if information is missing, say so
425
559
 
426
- \`\`\`typescript
427
- background_task(agent="general", prompt="[Specific task instructions]...")
428
- \`\`\`
560
+ ### Knowledge Persistence (You Do This Directly)
429
561
 
430
- Use for: parallel scans, research, data gathering that doesn't need session context.
562
+ When new information emerges that should be remembered:
431
563
 
432
- ### Skills
564
+ 1. **Read before write** \u2014 Always check existing content first
565
+ 2. **Smart Merge** \u2014 Integrate into existing sections, don't just append
566
+ 3. **Use templates** from \`kernel/templates/\` for new files
567
+ 4. **Update registries** \u2014 Add new files to relevant \`_index.md\` and \`registry.md\`
568
+ 5. **Bidirectional links** \u2014 If A references B, add A to B's related section
569
+ 6. **Chronicle significant events** \u2014 Append to \`chronicle.md\` with date stamp
570
+ 7. **Frontmatter required** \u2014 Every file needs type, hemisphere, dates, tags, summary
433
571
 
434
- Invoke with skill() tool. They provide detailed instructions for complex workflows. Available skills discovered at runtime \u2014 check .opencode/skill/ for what's available.
572
+ ### Deduplication Check (Before Creating Files)
435
573
 
436
- ### Sub-Agents
574
+ 1. Grep for entity name across knowledge base
575
+ 2. Check if file already exists
576
+ 3. If exists \u2192 UPDATE, not CREATE
577
+ 4. If similar exists \u2192 ASK for clarification
578
+
579
+ ## Functional Agents (For Specialized Tasks)
580
+
581
+ These agents handle tasks that DON'T require session context:
582
+
583
+ | Agent | Function | When to Use |
584
+ |-------|----------|-------------|
585
+ | **coach** | Reflection & thinking partner | "Help me think through X", "I'm stuck on Y", "Should I do A or B?" |
586
+ | **sentinel** | Proactive monitoring | "What needs my attention?", "Any overdue items?", "Check my calendar" |
587
+ | **diplomat** | Communication drafting | "Draft an email to X", "Help me respond to Y", "How should I say Z?" |
588
+ | **chronicler** | Meeting/event processing | "Process this meeting", "Extract action items" (from provided notes) |
589
+
590
+ ### When to Delegate vs Execute Directly
591
+
592
+ | Task | Action | Why |
593
+ |------|--------|-----|
594
+ | "What do I know about Sarah?" | **Execute directly** | Needs session context for relevance |
595
+ | "Remember that Sarah prefers async" | **Execute directly** | Needs session context for smart merge |
596
+ | "Help me think through this decision" | Delegate to **coach** | Specialized coaching framework |
597
+ | "What needs my attention today?" | Delegate to **sentinel** | Independent calendar/task scan |
598
+ | "Draft an email to Sarah" | Delegate to **diplomat** | Specialized communication drafting |
599
+ | "Process these meeting notes" | Delegate to **chronicler** | Structured extraction from provided text |
600
+
601
+ ### Background Agents (Parallel, Independent)
437
602
 
438
- For complex tasks requiring deep expertise, delegate to specialized sub-agents (Work-Master, Life-Master, Code-Master). Use task() tool with the 7-Section Format from Execution section.
603
+ For parallel research or data gathering, fire background agents:
604
+
605
+ \`\`\`typescript
606
+ // Example: Morning boot parallel scans
607
+ background_task(agent="general", prompt="[Email scan instructions]...")
608
+ background_task(agent="general", prompt="[Calendar scan instructions]...")
609
+ background_task(agent="general", prompt="[Slack scan instructions]...")
610
+ \`\`\`
611
+
612
+ These are appropriate because they gather independent data and return facts \u2014 they don't need session context.
439
613
  </Core_Capabilities>`;
440
614
  var THOTH_EXECUTION = `<Execution>
441
615
  ## Execution
@@ -468,6 +642,22 @@ A task is NOT complete without evidence:
468
642
  | Test run | Pass (or note pre-existing failures) |
469
643
  | Delegation | Agent result received and verified |
470
644
  </Execution>`;
645
+ var THOTH_TEMPORAL_AWARENESS = `<Temporal_Awareness>
646
+ ## Temporal Awareness (Chronos Protocol)
647
+
648
+ Operate with deep time awareness, respecting Zeus's biological and professional cycles.
649
+
650
+ ### Executive Calendar (Work)
651
+ - **Monday**: Launch Mode \u2014 Prioritize planning, alignment, P0 definition
652
+ - **Tue-Thu**: Execution Mode \u2014 Protect deep work blocks, minimize admin
653
+ - **Friday**: Closure Mode \u2014 Wrap up, delegation follow-ups, weekly review
654
+ - **Weekend**: Sanctuary \u2014 Block work unless Emergency P0
655
+
656
+ ### Biological Clock (Life)
657
+ - **Morning (08:00-11:00)**: High Cognitive \u2014 Protect from triage hell
658
+ - **Afternoon (14:00-17:00)**: Collaborative \u2014 Good for meetings and emails
659
+ - **Evening (19:00+)**: Restoration \u2014 Block work notifications, prompt journaling
660
+ </Temporal_Awareness>`;
471
661
  var THOTH_PERMISSIONS = `<Permission_System>
472
662
  ## Permission System
473
663
 
@@ -539,9 +729,15 @@ function buildThothPrompt(spec) {
539
729
  sections.push(THOTH_ANTI_PATTERNS);
540
730
  sections.push(THOTH_BEHAVIORAL_GUIDANCE);
541
731
  sections.push(THOTH_KNOWLEDGE_MANAGEMENT);
732
+ const skillRouting = buildSkillRoutingSection();
733
+ if (skillRouting) {
734
+ sections.push(skillRouting);
735
+ }
736
+ sections.push(THOTH_INTENT_GATE);
542
737
  sections.push(THOTH_CORE_CAPABILITIES);
543
738
  sections.push(THOTH_EXECUTION);
544
739
  sections.push(THOTH_PERMISSIONS);
740
+ sections.push(THOTH_TEMPORAL_AWARENESS);
545
741
  sections.push(THOTH_COMMUNICATION);
546
742
  sections.push(THOTH_CLOSING);
547
743
  return sections.join(`
@@ -3236,9 +3432,131 @@ To proceed, mark this as Emergency/P0 or wait until work hours.`
3236
3432
  getDayModeRecommendation
3237
3433
  };
3238
3434
  }
3435
+ // src/hooks/frontmatter-enforcer.ts
3436
+ import * as path5 from "path";
3437
+ var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
3438
+ function getToday() {
3439
+ return new Date().toISOString().split("T")[0];
3440
+ }
3441
+ function parseMarkdown(content) {
3442
+ const match = content.match(FRONTMATTER_REGEX);
3443
+ if (!match) {
3444
+ return { hasFrontmatter: false, frontmatter: {}, body: content };
3445
+ }
3446
+ const frontmatter = {};
3447
+ for (const line of match[1].split(`
3448
+ `)) {
3449
+ const colonIdx = line.indexOf(":");
3450
+ if (colonIdx === -1)
3451
+ continue;
3452
+ const key = line.slice(0, colonIdx).trim();
3453
+ let value = line.slice(colonIdx + 1).trim();
3454
+ if (value === "[]") {
3455
+ value = [];
3456
+ } else if (typeof value === "string") {
3457
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
3458
+ value = value.slice(1, -1);
3459
+ }
3460
+ }
3461
+ frontmatter[key] = value;
3462
+ }
3463
+ return {
3464
+ hasFrontmatter: true,
3465
+ frontmatter,
3466
+ body: content.slice(match[0].length)
3467
+ };
3468
+ }
3469
+ function serializeFrontmatter(data) {
3470
+ const lines = [];
3471
+ for (const [key, value] of Object.entries(data)) {
3472
+ if (Array.isArray(value)) {
3473
+ if (value.length === 0) {
3474
+ lines.push(`${key}: []`);
3475
+ } else {
3476
+ lines.push(`${key}:`);
3477
+ for (const item of value) {
3478
+ lines.push(` - ${item}`);
3479
+ }
3480
+ }
3481
+ } else if (typeof value === "string" && value.includes(":")) {
3482
+ lines.push(`${key}: "${value}"`);
3483
+ } else {
3484
+ lines.push(`${key}: ${value}`);
3485
+ }
3486
+ }
3487
+ return lines.join(`
3488
+ `);
3489
+ }
3490
+ function reconstructMarkdown(frontmatter, body) {
3491
+ return `---
3492
+ ${serializeFrontmatter(frontmatter)}
3493
+ ---
3494
+
3495
+ ${body.replace(/^\n+/, "")}`;
3496
+ }
3497
+ function ensureDates(content, created, updated) {
3498
+ const parsed = parseMarkdown(content);
3499
+ if (!parsed.hasFrontmatter) {
3500
+ return reconstructMarkdown({ created, updated }, content);
3501
+ }
3502
+ if (!parsed.frontmatter.created) {
3503
+ parsed.frontmatter.created = created;
3504
+ }
3505
+ parsed.frontmatter.updated = updated;
3506
+ return reconstructMarkdown(parsed.frontmatter, parsed.body);
3507
+ }
3508
+ function createFrontmatterEnforcerHook(config) {
3509
+ const { knowledgeBasePath, enabled = true } = config;
3510
+ if (!enabled)
3511
+ return null;
3512
+ const kbPath = expandPath(knowledgeBasePath);
3513
+ const pendingEdits = new Map;
3514
+ function isKbMarkdownFile(filePath) {
3515
+ if (!filePath.endsWith(".md"))
3516
+ return false;
3517
+ return path5.resolve(filePath).startsWith(path5.resolve(kbPath));
3518
+ }
3519
+ return {
3520
+ "tool.execute.before": async (input, output) => {
3521
+ const filePath = output.args.filePath;
3522
+ if (!filePath || !isKbMarkdownFile(filePath))
3523
+ return;
3524
+ if (input.tool === "write") {
3525
+ const content = output.args.content;
3526
+ if (!content)
3527
+ return;
3528
+ const today = getToday();
3529
+ const existing = readFileSync4(filePath);
3530
+ const createdDate = existing ? parseMarkdown(existing).frontmatter.created || today : today;
3531
+ output.args.content = ensureDates(content, createdDate, today);
3532
+ log(`Frontmatter: dates enforced for ${path5.basename(filePath)}`);
3533
+ }
3534
+ if (input.tool === "edit" && input.callID) {
3535
+ pendingEdits.set(input.callID, filePath);
3536
+ }
3537
+ },
3538
+ "tool.execute.after": async (input, _output) => {
3539
+ if (input.tool !== "edit")
3540
+ return;
3541
+ const filePath = pendingEdits.get(input.callID);
3542
+ pendingEdits.delete(input.callID);
3543
+ if (!filePath)
3544
+ return;
3545
+ const content = readFileSync4(filePath);
3546
+ if (!content)
3547
+ return;
3548
+ const parsed = parseMarkdown(content);
3549
+ if (!parsed.hasFrontmatter)
3550
+ return;
3551
+ parsed.frontmatter.updated = getToday();
3552
+ writeFileSync2(filePath, reconstructMarkdown(parsed.frontmatter, parsed.body));
3553
+ log(`Frontmatter: updated timestamp for ${path5.basename(filePath)}`);
3554
+ }
3555
+ };
3556
+ }
3239
3557
  // src/hooks/directory-agents-injector/index.ts
3240
3558
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3241
- import { dirname as dirname4, join as join8, resolve } from "path";
3559
+ import { dirname as dirname4, join as join9, resolve as resolve2 } from "path";
3242
3560
 
3243
3561
  // src/hooks/directory-agents-injector/storage.ts
3244
3562
  import {
@@ -3248,18 +3566,18 @@ import {
3248
3566
  writeFileSync as writeFileSync3,
3249
3567
  unlinkSync
3250
3568
  } from "fs";
3251
- import { join as join7 } from "path";
3569
+ import { join as join8 } from "path";
3252
3570
 
3253
3571
  // src/hooks/directory-agents-injector/constants.ts
3254
- import { join as join6 } from "path";
3255
- var xdgData = process.env.XDG_DATA_HOME || join6(process.env.HOME || "", ".local", "share");
3256
- var OPENCODE_STORAGE = join6(xdgData, "opencode", "storage");
3257
- var AGENTS_INJECTOR_STORAGE = join6(OPENCODE_STORAGE, "thoth-directory-agents");
3572
+ import { join as join7 } from "path";
3573
+ var xdgData = process.env.XDG_DATA_HOME || join7(process.env.HOME || "", ".local", "share");
3574
+ var OPENCODE_STORAGE = join7(xdgData, "opencode", "storage");
3575
+ var AGENTS_INJECTOR_STORAGE = join7(OPENCODE_STORAGE, "thoth-directory-agents");
3258
3576
  var AGENTS_FILENAME2 = "AGENTS.md";
3259
3577
 
3260
3578
  // src/hooks/directory-agents-injector/storage.ts
3261
3579
  function getStoragePath(sessionID) {
3262
- return join7(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
3580
+ return join8(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
3263
3581
  }
3264
3582
  function loadInjectedPaths(sessionID) {
3265
3583
  const filePath = getStoragePath(sessionID);
@@ -3308,14 +3626,14 @@ function createDirectoryAgentsInjectorHook(options) {
3308
3626
  return null;
3309
3627
  if (title.startsWith("/"))
3310
3628
  return title;
3311
- return resolve(directory, title);
3629
+ return resolve2(directory, title);
3312
3630
  }
3313
3631
  function findAgentsMdUp(startDir) {
3314
3632
  const found = [];
3315
3633
  let current = startDir;
3316
3634
  const normalizedBase = knowledgeBasePath.endsWith("/") ? knowledgeBasePath.slice(0, -1) : knowledgeBasePath;
3317
3635
  while (true) {
3318
- const agentsPath = join8(current, AGENTS_FILENAME2);
3636
+ const agentsPath = join9(current, AGENTS_FILENAME2);
3319
3637
  if (existsSync6(agentsPath)) {
3320
3638
  found.push(agentsPath);
3321
3639
  }
@@ -3360,8 +3678,8 @@ function createDirectoryAgentsInjectorHook(options) {
3360
3678
  }
3361
3679
  if (toInject.length === 0)
3362
3680
  return;
3363
- for (const { path: path5, content } of toInject) {
3364
- const relativePath = path5.replace(knowledgeBasePath, "").replace(/^\//, "");
3681
+ for (const { path: path6, content } of toInject) {
3682
+ const relativePath = path6.replace(knowledgeBasePath, "").replace(/^\//, "");
3365
3683
  output.output += `
3366
3684
 
3367
3685
  [Directory Context: ${relativePath}]
@@ -3396,8 +3714,8 @@ ${content}`;
3396
3714
  }
3397
3715
 
3398
3716
  // src/shared-hooks/todo-continuation-enforcer.ts
3399
- import { existsSync as existsSync8, readdirSync as readdirSync2 } from "fs";
3400
- import { join as join12 } from "path";
3717
+ import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
3718
+ import { join as join13 } from "path";
3401
3719
 
3402
3720
  // src/shared-hooks/utils/session-state.ts
3403
3721
  var subagentSessions = new Set;
@@ -3410,16 +3728,16 @@ function getMainSessionID() {
3410
3728
  }
3411
3729
 
3412
3730
  // src/shared-hooks/utils/message-storage.ts
3413
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync, writeFileSync as writeFileSync4 } from "fs";
3414
- import { join as join10 } from "path";
3731
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
3732
+ import { join as join11 } from "path";
3415
3733
 
3416
3734
  // src/shared-hooks/utils/constants.ts
3417
- import { join as join9 } from "path";
3418
- import { homedir as homedir2 } from "os";
3419
- var xdgData2 = process.env.XDG_DATA_HOME || join9(homedir2(), ".local", "share");
3420
- var OPENCODE_STORAGE2 = join9(xdgData2, "opencode", "storage");
3421
- var MESSAGE_STORAGE = join9(OPENCODE_STORAGE2, "message");
3422
- var PART_STORAGE = join9(OPENCODE_STORAGE2, "part");
3735
+ import { join as join10 } from "path";
3736
+ import { homedir as homedir3 } from "os";
3737
+ var xdgData2 = process.env.XDG_DATA_HOME || join10(homedir3(), ".local", "share");
3738
+ var OPENCODE_STORAGE2 = join10(xdgData2, "opencode", "storage");
3739
+ var MESSAGE_STORAGE = join10(OPENCODE_STORAGE2, "message");
3740
+ var PART_STORAGE = join10(OPENCODE_STORAGE2, "part");
3423
3741
  var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
3424
3742
  var META_TYPES = new Set(["step-start", "step-finish"]);
3425
3743
  var CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"]);
@@ -3427,10 +3745,10 @@ var CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"]);
3427
3745
  // src/shared-hooks/utils/message-storage.ts
3428
3746
  function findNearestMessageWithFields(messageDir) {
3429
3747
  try {
3430
- const files = readdirSync(messageDir).filter((f) => f.endsWith(".json")).sort().reverse();
3748
+ const files = readdirSync2(messageDir).filter((f) => f.endsWith(".json")).sort().reverse();
3431
3749
  for (const file of files) {
3432
3750
  try {
3433
- const content = readFileSync7(join10(messageDir, file), "utf-8");
3751
+ const content = readFileSync7(join11(messageDir, file), "utf-8");
3434
3752
  const msg = JSON.parse(content);
3435
3753
  if (msg.agent && msg.model?.providerID && msg.model?.modelID) {
3436
3754
  return msg;
@@ -3448,8 +3766,8 @@ function findNearestMessageWithFields(messageDir) {
3448
3766
  // src/shared-hooks/utils/logger.ts
3449
3767
  import * as fs2 from "fs";
3450
3768
  import * as os2 from "os";
3451
- import * as path5 from "path";
3452
- var logFile = path5.join(os2.tmpdir(), "thoth-plugin.log");
3769
+ import * as path6 from "path";
3770
+ var logFile = path6.join(os2.tmpdir(), "thoth-plugin.log");
3453
3771
  function log2(message, data) {
3454
3772
  try {
3455
3773
  const timestamp = new Date().toISOString();
@@ -3471,11 +3789,11 @@ Incomplete tasks remain in your todo list. Continue working on the next pending
3471
3789
  function getMessageDir(sessionID) {
3472
3790
  if (!existsSync8(MESSAGE_STORAGE))
3473
3791
  return null;
3474
- const directPath = join12(MESSAGE_STORAGE, sessionID);
3792
+ const directPath = join13(MESSAGE_STORAGE, sessionID);
3475
3793
  if (existsSync8(directPath))
3476
3794
  return directPath;
3477
- for (const dir of readdirSync2(MESSAGE_STORAGE)) {
3478
- const sessionPath = join12(MESSAGE_STORAGE, dir, sessionID);
3795
+ for (const dir of readdirSync3(MESSAGE_STORAGE)) {
3796
+ const sessionPath = join13(MESSAGE_STORAGE, dir, sessionID);
3479
3797
  if (existsSync8(sessionPath))
3480
3798
  return sessionPath;
3481
3799
  }
@@ -3703,16 +4021,16 @@ function createTodoContinuationEnforcer(ctx) {
3703
4021
  };
3704
4022
  }
3705
4023
  // src/shared-hooks/session-recovery/storage.ts
3706
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
3707
- import { join as join14 } from "path";
4024
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
4025
+ import { join as join15 } from "path";
3708
4026
 
3709
4027
  // src/shared-hooks/session-recovery/constants.ts
3710
- import { join as join13 } from "path";
3711
- import { homedir as homedir3 } from "os";
3712
- var xdgData3 = process.env.XDG_DATA_HOME || join13(homedir3(), ".local", "share");
3713
- var OPENCODE_STORAGE3 = join13(xdgData3, "opencode", "storage");
3714
- var MESSAGE_STORAGE2 = join13(OPENCODE_STORAGE3, "message");
3715
- var PART_STORAGE2 = join13(OPENCODE_STORAGE3, "part");
4028
+ import { join as join14 } from "path";
4029
+ import { homedir as homedir4 } from "os";
4030
+ var xdgData3 = process.env.XDG_DATA_HOME || join14(homedir4(), ".local", "share");
4031
+ var OPENCODE_STORAGE3 = join14(xdgData3, "opencode", "storage");
4032
+ var MESSAGE_STORAGE2 = join14(OPENCODE_STORAGE3, "message");
4033
+ var PART_STORAGE2 = join14(OPENCODE_STORAGE3, "part");
3716
4034
  var THINKING_TYPES2 = new Set(["thinking", "redacted_thinking", "reasoning"]);
3717
4035
  var META_TYPES2 = new Set(["step-start", "step-finish"]);
3718
4036
  var CONTENT_TYPES2 = new Set(["text", "tool", "tool_use", "tool_result"]);
@@ -3721,12 +4039,12 @@ var CONTENT_TYPES2 = new Set(["text", "tool", "tool_use", "tool_result"]);
3721
4039
  function getMessageDir2(sessionID) {
3722
4040
  if (!existsSync9(MESSAGE_STORAGE2))
3723
4041
  return "";
3724
- const directPath = join14(MESSAGE_STORAGE2, sessionID);
4042
+ const directPath = join15(MESSAGE_STORAGE2, sessionID);
3725
4043
  if (existsSync9(directPath)) {
3726
4044
  return directPath;
3727
4045
  }
3728
- for (const dir of readdirSync3(MESSAGE_STORAGE2)) {
3729
- const sessionPath = join14(MESSAGE_STORAGE2, dir, sessionID);
4046
+ for (const dir of readdirSync4(MESSAGE_STORAGE2)) {
4047
+ const sessionPath = join15(MESSAGE_STORAGE2, dir, sessionID);
3730
4048
  if (existsSync9(sessionPath)) {
3731
4049
  return sessionPath;
3732
4050
  }
@@ -3738,11 +4056,11 @@ function readMessages(sessionID) {
3738
4056
  if (!messageDir || !existsSync9(messageDir))
3739
4057
  return [];
3740
4058
  const messages = [];
3741
- for (const file of readdirSync3(messageDir)) {
4059
+ for (const file of readdirSync4(messageDir)) {
3742
4060
  if (!file.endsWith(".json"))
3743
4061
  continue;
3744
4062
  try {
3745
- const content = readFileSync8(join14(messageDir, file), "utf-8");
4063
+ const content = readFileSync8(join15(messageDir, file), "utf-8");
3746
4064
  messages.push(JSON.parse(content));
3747
4065
  } catch {
3748
4066
  continue;
@@ -3757,15 +4075,15 @@ function readMessages(sessionID) {
3757
4075
  });
3758
4076
  }
3759
4077
  function readParts(messageID) {
3760
- const partDir = join14(PART_STORAGE2, messageID);
4078
+ const partDir = join15(PART_STORAGE2, messageID);
3761
4079
  if (!existsSync9(partDir))
3762
4080
  return [];
3763
4081
  const parts = [];
3764
- for (const file of readdirSync3(partDir)) {
4082
+ for (const file of readdirSync4(partDir)) {
3765
4083
  if (!file.endsWith(".json"))
3766
4084
  continue;
3767
4085
  try {
3768
- const content = readFileSync8(join14(partDir, file), "utf-8");
4086
+ const content = readFileSync8(join15(partDir, file), "utf-8");
3769
4087
  parts.push(JSON.parse(content));
3770
4088
  } catch {
3771
4089
  continue;
@@ -3807,7 +4125,7 @@ function findMessagesWithOrphanThinking(sessionID) {
3807
4125
  return result;
3808
4126
  }
3809
4127
  function prependThinkingPart(sessionID, messageID) {
3810
- const partDir = join14(PART_STORAGE2, messageID);
4128
+ const partDir = join15(PART_STORAGE2, messageID);
3811
4129
  if (!existsSync9(partDir)) {
3812
4130
  mkdirSync4(partDir, { recursive: true });
3813
4131
  }
@@ -3821,22 +4139,22 @@ function prependThinkingPart(sessionID, messageID) {
3821
4139
  synthetic: true
3822
4140
  };
3823
4141
  try {
3824
- writeFileSync5(join14(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
4142
+ writeFileSync5(join15(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
3825
4143
  return true;
3826
4144
  } catch {
3827
4145
  return false;
3828
4146
  }
3829
4147
  }
3830
4148
  function stripThinkingParts(messageID) {
3831
- const partDir = join14(PART_STORAGE2, messageID);
4149
+ const partDir = join15(PART_STORAGE2, messageID);
3832
4150
  if (!existsSync9(partDir))
3833
4151
  return false;
3834
4152
  let anyRemoved = false;
3835
- for (const file of readdirSync3(partDir)) {
4153
+ for (const file of readdirSync4(partDir)) {
3836
4154
  if (!file.endsWith(".json"))
3837
4155
  continue;
3838
4156
  try {
3839
- const filePath = join14(partDir, file);
4157
+ const filePath = join15(partDir, file);
3840
4158
  const content = readFileSync8(filePath, "utf-8");
3841
4159
  const part = JSON.parse(content);
3842
4160
  if (THINKING_TYPES2.has(part.type)) {
@@ -4163,16 +4481,16 @@ ${CONTEXT_REMINDER}
4163
4481
  };
4164
4482
  }
4165
4483
  // src/shared-hooks/background-agent/manager.ts
4166
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
4167
- import { join as join15 } from "path";
4484
+ import { existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
4485
+ import { join as join16 } from "path";
4168
4486
  function getMessageDir3(sessionID) {
4169
4487
  if (!existsSync10(MESSAGE_STORAGE))
4170
4488
  return null;
4171
- const directPath = join15(MESSAGE_STORAGE, sessionID);
4489
+ const directPath = join16(MESSAGE_STORAGE, sessionID);
4172
4490
  if (existsSync10(directPath))
4173
4491
  return directPath;
4174
- for (const dir of readdirSync4(MESSAGE_STORAGE)) {
4175
- const sessionPath = join15(MESSAGE_STORAGE, dir, sessionID);
4492
+ for (const dir of readdirSync5(MESSAGE_STORAGE)) {
4493
+ const sessionPath = join16(MESSAGE_STORAGE, dir, sessionID);
4176
4494
  if (existsSync10(sessionPath))
4177
4495
  return sessionPath;
4178
4496
  }
@@ -5249,10 +5567,10 @@ function mergeDefs(...defs) {
5249
5567
  function cloneDef(schema) {
5250
5568
  return mergeDefs(schema._zod.def);
5251
5569
  }
5252
- function getElementAtPath(obj, path6) {
5253
- if (!path6)
5570
+ function getElementAtPath(obj, path7) {
5571
+ if (!path7)
5254
5572
  return obj;
5255
- return path6.reduce((acc, key) => acc?.[key], obj);
5573
+ return path7.reduce((acc, key) => acc?.[key], obj);
5256
5574
  }
5257
5575
  function promiseAllObject(promisesObj) {
5258
5576
  const keys = Object.keys(promisesObj);
@@ -5611,11 +5929,11 @@ function aborted(x, startIndex = 0) {
5611
5929
  }
5612
5930
  return false;
5613
5931
  }
5614
- function prefixIssues(path6, issues) {
5932
+ function prefixIssues(path7, issues) {
5615
5933
  return issues.map((iss) => {
5616
5934
  var _a;
5617
5935
  (_a = iss).path ?? (_a.path = []);
5618
- iss.path.unshift(path6);
5936
+ iss.path.unshift(path7);
5619
5937
  return iss;
5620
5938
  });
5621
5939
  }
@@ -5783,7 +6101,7 @@ function treeifyError(error, _mapper) {
5783
6101
  return issue2.message;
5784
6102
  };
5785
6103
  const result = { errors: [] };
5786
- const processError = (error2, path6 = []) => {
6104
+ const processError = (error2, path7 = []) => {
5787
6105
  var _a, _b;
5788
6106
  for (const issue2 of error2.issues) {
5789
6107
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -5793,7 +6111,7 @@ function treeifyError(error, _mapper) {
5793
6111
  } else if (issue2.code === "invalid_element") {
5794
6112
  processError({ issues: issue2.issues }, issue2.path);
5795
6113
  } else {
5796
- const fullpath = [...path6, ...issue2.path];
6114
+ const fullpath = [...path7, ...issue2.path];
5797
6115
  if (fullpath.length === 0) {
5798
6116
  result.errors.push(mapper(issue2));
5799
6117
  continue;
@@ -5825,8 +6143,8 @@ function treeifyError(error, _mapper) {
5825
6143
  }
5826
6144
  function toDotPath(_path) {
5827
6145
  const segs = [];
5828
- const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5829
- for (const seg of path6) {
6146
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6147
+ for (const seg of path7) {
5830
6148
  if (typeof seg === "number")
5831
6149
  segs.push(`[${seg}]`);
5832
6150
  else if (typeof seg === "symbol")
@@ -16901,7 +17219,7 @@ Use \`background_output\` tool with task_id="${task.id}" to check progress:
16901
17219
  });
16902
17220
  }
16903
17221
  function delay(ms) {
16904
- return new Promise((resolve2) => setTimeout(resolve2, ms));
17222
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
16905
17223
  }
16906
17224
  function truncateText(text, maxLength) {
16907
17225
  if (text.length <= maxLength)
@@ -17115,13 +17433,13 @@ Status: ${task.status}`;
17115
17433
  });
17116
17434
  }
17117
17435
  // src/tools/skill/tools.ts
17118
- import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync9, lstatSync, readlinkSync } from "fs";
17119
- import { homedir as homedir4 } from "os";
17120
- import { join as join16, basename as basename2, resolve as resolve2, dirname as dirname5 } from "path";
17436
+ import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync9, lstatSync, readlinkSync } from "fs";
17437
+ import { homedir as homedir5 } from "os";
17438
+ import { join as join17, basename as basename3, resolve as resolve3, dirname as dirname5 } from "path";
17121
17439
  import { fileURLToPath as fileURLToPath2 } from "url";
17122
17440
  var __filename3 = fileURLToPath2(import.meta.url);
17123
17441
  var __dirname3 = dirname5(__filename3);
17124
- var NPM_DEFAULTS_SKILL_DIR = join16(__dirname3, "..", "..", "defaults", "skill");
17442
+ var NPM_DEFAULTS_SKILL_DIR = join17(__dirname3, "..", "..", "defaults", "skill");
17125
17443
  function deduplicateSkills(skills) {
17126
17444
  const seen = new Set;
17127
17445
  return skills.filter((skill) => {
@@ -17181,7 +17499,7 @@ function resolveSymlink(filePath) {
17181
17499
  try {
17182
17500
  const stats = lstatSync(filePath, { throwIfNoEntry: false });
17183
17501
  if (stats?.isSymbolicLink()) {
17184
- return resolve2(filePath, "..", readlinkSync(filePath));
17502
+ return resolve3(filePath, "..", readlinkSync(filePath));
17185
17503
  }
17186
17504
  return filePath;
17187
17505
  } catch {
@@ -17194,6 +17512,7 @@ function parseSkillFrontmatter(data) {
17194
17512
  description: typeof data.description === "string" ? data.description : "",
17195
17513
  license: typeof data.license === "string" ? data.license : undefined,
17196
17514
  "allowed-tools": Array.isArray(data["allowed-tools"]) ? data["allowed-tools"] : undefined,
17515
+ triggers: Array.isArray(data.triggers) ? data.triggers : undefined,
17197
17516
  metadata: typeof data.metadata === "object" && data.metadata !== null ? data.metadata : undefined
17198
17517
  };
17199
17518
  }
@@ -17201,15 +17520,15 @@ function discoverSkillsFromDir(skillsDir, scope) {
17201
17520
  if (!existsSync11(skillsDir)) {
17202
17521
  return [];
17203
17522
  }
17204
- const entries = readdirSync5(skillsDir, { withFileTypes: true });
17523
+ const entries = readdirSync6(skillsDir, { withFileTypes: true });
17205
17524
  const skills = [];
17206
17525
  for (const entry of entries) {
17207
17526
  if (entry.name.startsWith("."))
17208
17527
  continue;
17209
- const skillPath = join16(skillsDir, entry.name);
17528
+ const skillPath = join17(skillsDir, entry.name);
17210
17529
  if (entry.isDirectory() || entry.isSymbolicLink()) {
17211
17530
  const resolvedPath = resolveSymlink(skillPath);
17212
- const skillMdPath = join16(resolvedPath, "SKILL.md");
17531
+ const skillMdPath = join17(resolvedPath, "SKILL.md");
17213
17532
  if (!existsSync11(skillMdPath))
17214
17533
  continue;
17215
17534
  try {
@@ -17218,7 +17537,8 @@ function discoverSkillsFromDir(skillsDir, scope) {
17218
17537
  skills.push({
17219
17538
  name: data.name || entry.name,
17220
17539
  description: data.description || "",
17221
- scope
17540
+ scope,
17541
+ triggers: Array.isArray(data.triggers) ? data.triggers : undefined
17222
17542
  });
17223
17543
  } catch {
17224
17544
  continue;
@@ -17228,16 +17548,35 @@ function discoverSkillsFromDir(skillsDir, scope) {
17228
17548
  return skills;
17229
17549
  }
17230
17550
  function discoverSkillsSync() {
17231
- const userSkillsDir = join16(homedir4(), ".opencode", "skill");
17232
- const projectSkillsDir = join16(process.cwd(), ".opencode", "skill");
17551
+ const userSkillsDir = join17(homedir5(), ".opencode", "skill");
17552
+ const projectSkillsDir = join17(process.cwd(), ".opencode", "skill");
17233
17553
  const builtinSkills = discoverSkillsFromDir(NPM_DEFAULTS_SKILL_DIR, "builtin");
17234
17554
  const userSkills = discoverSkillsFromDir(userSkillsDir, "user");
17235
17555
  const projectSkills = discoverSkillsFromDir(projectSkillsDir, "project");
17236
17556
  return deduplicateSkills([...projectSkills, ...userSkills, ...builtinSkills]);
17237
17557
  }
17558
+ function buildTriggerSection(skills) {
17559
+ const skillsWithTriggers = skills.filter((s) => s.triggers && s.triggers.length > 0);
17560
+ if (skillsWithTriggers.length === 0) {
17561
+ return "";
17562
+ }
17563
+ const lines = [
17564
+ `
17565
+
17566
+ Skill Triggers (invoke skill when user says these phrases):`
17567
+ ];
17568
+ for (const skill of skillsWithTriggers) {
17569
+ const triggers = skill.triggers.map((t) => `"${t}"`).join(", ");
17570
+ lines.push(`- ${triggers} \u2192 ${skill.name}`);
17571
+ }
17572
+ lines.push(`
17573
+ IMPORTANT: If user intent matches a trigger phrase, invoke the skill immediately. Don't improvise.`);
17574
+ return lines.join(`
17575
+ `);
17576
+ }
17238
17577
  async function parseSkillMd(skillPath) {
17239
17578
  const resolvedPath = resolveSymlink(skillPath);
17240
- const skillMdPath = join16(resolvedPath, "SKILL.md");
17579
+ const skillMdPath = join17(resolvedPath, "SKILL.md");
17241
17580
  if (!existsSync11(skillMdPath)) {
17242
17581
  return null;
17243
17582
  }
@@ -17246,18 +17585,19 @@ async function parseSkillMd(skillPath) {
17246
17585
  const { data, body } = parseFrontmatter(content);
17247
17586
  const frontmatter = parseSkillFrontmatter(data);
17248
17587
  const metadata = {
17249
- name: frontmatter.name || basename2(skillPath),
17588
+ name: frontmatter.name || basename3(skillPath),
17250
17589
  description: frontmatter.description,
17251
17590
  license: frontmatter.license,
17252
17591
  allowedTools: frontmatter["allowed-tools"],
17592
+ triggers: frontmatter.triggers,
17253
17593
  metadata: frontmatter.metadata
17254
17594
  };
17255
- const referencesDir = join16(resolvedPath, "references");
17256
- const scriptsDir = join16(resolvedPath, "scripts");
17257
- const assetsDir = join16(resolvedPath, "assets");
17258
- const references = existsSync11(referencesDir) ? readdirSync5(referencesDir).filter((f) => !f.startsWith(".")) : [];
17259
- const scripts = existsSync11(scriptsDir) ? readdirSync5(scriptsDir).filter((f) => !f.startsWith(".") && !f.startsWith("__")) : [];
17260
- const assets = existsSync11(assetsDir) ? readdirSync5(assetsDir).filter((f) => !f.startsWith(".")) : [];
17595
+ const referencesDir = join17(resolvedPath, "references");
17596
+ const scriptsDir = join17(resolvedPath, "scripts");
17597
+ const assetsDir = join17(resolvedPath, "assets");
17598
+ const references = existsSync11(referencesDir) ? readdirSync6(referencesDir).filter((f) => !f.startsWith(".")) : [];
17599
+ const scripts = existsSync11(scriptsDir) ? readdirSync6(scriptsDir).filter((f) => !f.startsWith(".") && !f.startsWith("__")) : [];
17600
+ const assets = existsSync11(assetsDir) ? readdirSync6(assetsDir).filter((f) => !f.startsWith(".")) : [];
17261
17601
  return {
17262
17602
  name: metadata.name,
17263
17603
  path: resolvedPath,
@@ -17276,12 +17616,12 @@ async function discoverSkillsFromDirAsync(skillsDir) {
17276
17616
  if (!existsSync11(skillsDir)) {
17277
17617
  return [];
17278
17618
  }
17279
- const entries = readdirSync5(skillsDir, { withFileTypes: true });
17619
+ const entries = readdirSync6(skillsDir, { withFileTypes: true });
17280
17620
  const skills = [];
17281
17621
  for (const entry of entries) {
17282
17622
  if (entry.name.startsWith("."))
17283
17623
  continue;
17284
- const skillPath = join16(skillsDir, entry.name);
17624
+ const skillPath = join17(skillsDir, entry.name);
17285
17625
  if (entry.isDirectory() || entry.isSymbolicLink()) {
17286
17626
  const skillInfo = await parseSkillMd(skillPath);
17287
17627
  if (skillInfo) {
@@ -17292,8 +17632,8 @@ async function discoverSkillsFromDirAsync(skillsDir) {
17292
17632
  return skills;
17293
17633
  }
17294
17634
  async function discoverSkills() {
17295
- const userSkillsDir = join16(homedir4(), ".opencode", "skill");
17296
- const projectSkillsDir = join16(process.cwd(), ".opencode", "skill");
17635
+ const userSkillsDir = join17(homedir5(), ".opencode", "skill");
17636
+ const projectSkillsDir = join17(process.cwd(), ".opencode", "skill");
17297
17637
  const builtinSkills = await discoverSkillsFromDirAsync(NPM_DEFAULTS_SKILL_DIR);
17298
17638
  const userSkills = await discoverSkillsFromDirAsync(userSkillsDir);
17299
17639
  const projectSkills = await discoverSkillsFromDirAsync(projectSkillsDir);
@@ -17323,7 +17663,7 @@ async function loadSkillWithReferences(skill, includeRefs) {
17323
17663
  const referencesLoaded = [];
17324
17664
  if (includeRefs && skill.references.length > 0) {
17325
17665
  for (const ref of skill.references) {
17326
- const refPath = join16(skill.path, "references", ref);
17666
+ const refPath = join17(skill.path, "references", ref);
17327
17667
  try {
17328
17668
  const content = readFileSync9(refPath, "utf-8");
17329
17669
  referencesLoaded.push({ path: ref, content });
@@ -17384,13 +17724,14 @@ function createSkillTool() {
17384
17724
  const availableSkills = discoverSkillsSync();
17385
17725
  const skillListForDescription = availableSkills.map((s) => `- ${s.name}: ${s.description} (${s.scope})`).join(`
17386
17726
  `);
17727
+ const triggerSection = buildTriggerSection(availableSkills);
17387
17728
  return tool({
17388
17729
  description: `Execute a skill within the main conversation.
17389
17730
 
17390
17731
  When you invoke a skill, the skill's prompt will expand and provide detailed instructions on how to complete the task.
17391
17732
 
17392
17733
  Available Skills:
17393
- ${skillListForDescription || "(No skills discovered yet)"}`,
17734
+ ${skillListForDescription || "(No skills discovered yet)"}${triggerSection}`,
17394
17735
  args: {
17395
17736
  skill: tool.schema.string().describe("The skill name or search query to find and load. Can be exact skill name (e.g., 'morning-boot') or keywords (e.g., 'morning', 'boot').")
17396
17737
  },
@@ -18183,10 +18524,10 @@ function mergeDefs2(...defs) {
18183
18524
  function cloneDef2(schema) {
18184
18525
  return mergeDefs2(schema._zod.def);
18185
18526
  }
18186
- function getElementAtPath2(obj, path6) {
18187
- if (!path6)
18527
+ function getElementAtPath2(obj, path7) {
18528
+ if (!path7)
18188
18529
  return obj;
18189
- return path6.reduce((acc, key) => acc?.[key], obj);
18530
+ return path7.reduce((acc, key) => acc?.[key], obj);
18190
18531
  }
18191
18532
  function promiseAllObject2(promisesObj) {
18192
18533
  const keys = Object.keys(promisesObj);
@@ -18567,11 +18908,11 @@ function aborted2(x, startIndex = 0) {
18567
18908
  }
18568
18909
  return false;
18569
18910
  }
18570
- function prefixIssues2(path6, issues) {
18911
+ function prefixIssues2(path7, issues) {
18571
18912
  return issues.map((iss) => {
18572
18913
  var _a;
18573
18914
  (_a = iss).path ?? (_a.path = []);
18574
- iss.path.unshift(path6);
18915
+ iss.path.unshift(path7);
18575
18916
  return iss;
18576
18917
  });
18577
18918
  }
@@ -18754,7 +19095,7 @@ function formatError2(error45, mapper = (issue3) => issue3.message) {
18754
19095
  }
18755
19096
  function treeifyError2(error45, mapper = (issue3) => issue3.message) {
18756
19097
  const result = { errors: [] };
18757
- const processError = (error46, path6 = []) => {
19098
+ const processError = (error46, path7 = []) => {
18758
19099
  var _a, _b;
18759
19100
  for (const issue3 of error46.issues) {
18760
19101
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -18764,7 +19105,7 @@ function treeifyError2(error45, mapper = (issue3) => issue3.message) {
18764
19105
  } else if (issue3.code === "invalid_element") {
18765
19106
  processError({ issues: issue3.issues }, issue3.path);
18766
19107
  } else {
18767
- const fullpath = [...path6, ...issue3.path];
19108
+ const fullpath = [...path7, ...issue3.path];
18768
19109
  if (fullpath.length === 0) {
18769
19110
  result.errors.push(mapper(issue3));
18770
19111
  continue;
@@ -18796,8 +19137,8 @@ function treeifyError2(error45, mapper = (issue3) => issue3.message) {
18796
19137
  }
18797
19138
  function toDotPath2(_path) {
18798
19139
  const segs = [];
18799
- const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
18800
- for (const seg of path6) {
19140
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
19141
+ for (const seg of path7) {
18801
19142
  if (typeof seg === "number")
18802
19143
  segs.push(`[${seg}]`);
18803
19144
  else if (typeof seg === "symbol")
@@ -30544,13 +30885,13 @@ function resolveRef(ref, ctx) {
30544
30885
  if (!ref.startsWith("#")) {
30545
30886
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
30546
30887
  }
30547
- const path6 = ref.slice(1).split("/").filter(Boolean);
30548
- if (path6.length === 0) {
30888
+ const path7 = ref.slice(1).split("/").filter(Boolean);
30889
+ if (path7.length === 0) {
30549
30890
  return ctx.rootSchema;
30550
30891
  }
30551
30892
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
30552
- if (path6[0] === defsKey) {
30553
- const key = path6[1];
30893
+ if (path7[0] === defsKey) {
30894
+ const key = path7[1];
30554
30895
  if (!key || !ctx.defs[key]) {
30555
30896
  throw new Error(`Reference not found: ${ref}`);
30556
30897
  }
@@ -30964,6 +31305,7 @@ var HooksConfigSchema = exports_external2.object({
30964
31305
  "temporal-awareness": exports_external2.boolean().optional(),
30965
31306
  "knowledge-persistence": exports_external2.boolean().optional(),
30966
31307
  "directory-agents-injector": exports_external2.boolean().optional(),
31308
+ "frontmatter-enforcer": exports_external2.boolean().optional(),
30967
31309
  "todo-continuation": exports_external2.boolean().optional(),
30968
31310
  "session-recovery": exports_external2.boolean().optional(),
30969
31311
  "context-window-monitor": exports_external2.boolean().optional(),
@@ -31001,7 +31343,7 @@ var ThothPluginConfigSchema = exports_external2.object({
31001
31343
  }).strict();
31002
31344
  // src/index.ts
31003
31345
  import * as fs3 from "fs";
31004
- import * as path6 from "path";
31346
+ import * as path7 from "path";
31005
31347
  var sessionSpecializations = new Map;
31006
31348
  function loadConfigFromPath(configPath) {
31007
31349
  try {
@@ -31033,8 +31375,8 @@ function mergeConfigs(base, override) {
31033
31375
  };
31034
31376
  }
31035
31377
  function loadPluginConfig(directory) {
31036
- const userConfigPath = path6.join(getUserConfigDir(), "opencode", "thoth-plugin.json");
31037
- const projectConfigPath = path6.join(directory, ".opencode", "thoth-plugin.json");
31378
+ const userConfigPath = path7.join(getUserConfigDir(), "opencode", "thoth-plugin.json");
31379
+ const projectConfigPath = path7.join(directory, ".opencode", "thoth-plugin.json");
31038
31380
  let config3 = loadConfigFromPath(userConfigPath) ?? {};
31039
31381
  const projectConfig = loadConfigFromPath(projectConfigPath);
31040
31382
  if (projectConfig) {
@@ -31048,15 +31390,15 @@ function resolveKnowledgeBasePath(config3, directory) {
31048
31390
  return expandPath(config3.knowledge_base);
31049
31391
  }
31050
31392
  const commonLocations = [
31051
- path6.join(process.env.HOME || "", "Repos", "thoth"),
31052
- path6.join(process.env.HOME || "", "repos", "thoth"),
31053
- path6.join(process.env.HOME || "", "Projects", "thoth"),
31054
- path6.join(process.env.HOME || "", "projects", "thoth"),
31055
- path6.join(process.env.HOME || "", "thoth"),
31056
- path6.join(directory, "thoth")
31393
+ path7.join(process.env.HOME || "", "Repos", "thoth"),
31394
+ path7.join(process.env.HOME || "", "repos", "thoth"),
31395
+ path7.join(process.env.HOME || "", "Projects", "thoth"),
31396
+ path7.join(process.env.HOME || "", "projects", "thoth"),
31397
+ path7.join(process.env.HOME || "", "thoth"),
31398
+ path7.join(directory, "thoth")
31057
31399
  ];
31058
31400
  for (const location of commonLocations) {
31059
- const kernelPath = path6.join(location, "kernel");
31401
+ const kernelPath = path7.join(location, "kernel");
31060
31402
  if (fs3.existsSync(kernelPath)) {
31061
31403
  log(`Found knowledge base at: ${location}`);
31062
31404
  return location;
@@ -31077,6 +31419,7 @@ var ThothPlugin = async (ctx) => {
31077
31419
  const trustLevelTracker = hooksConfig["trust-level-tracker"] !== false ? createTrustLevelTrackerHook({ knowledgeBasePath }) : null;
31078
31420
  const contextAperture = hooksConfig["context-aperture"] !== false ? createContextApertureHook({ knowledgeBasePath }) : null;
31079
31421
  const temporalAwareness = hooksConfig["temporal-awareness"] !== false ? createTemporalAwarenessHook() : null;
31422
+ const frontmatterEnforcer = hooksConfig["frontmatter-enforcer"] !== false ? createFrontmatterEnforcerHook({ knowledgeBasePath }) : null;
31080
31423
  const todoContinuationEnforcer = hooksConfig["todo-continuation"] !== false ? createTodoContinuationEnforcer(ctx) : null;
31081
31424
  const sessionRecoveryHook = hooksConfig["session-recovery"] !== false ? createSessionRecoveryHook(ctx, { experimental: { auto_resume: true } }) : null;
31082
31425
  const contextWindowMonitor = hooksConfig["context-window-monitor"] !== false ? createContextWindowMonitorHook(ctx) : null;
@@ -31181,10 +31524,12 @@ var ThothPlugin = async (ctx) => {
31181
31524
  await temporalAwareness?.["tool.execute.before"]?.(input, output);
31182
31525
  await contextAperture?.["tool.execute.before"]?.(input, output);
31183
31526
  await trustLevelTracker?.["tool.execute.before"]?.(input, output);
31527
+ await frontmatterEnforcer?.["tool.execute.before"]?.(input, output);
31184
31528
  },
31185
31529
  "tool.execute.after": async (input, output) => {
31186
31530
  await trustLevelTracker?.["tool.execute.after"]?.(input, output);
31187
31531
  await contextAperture?.["tool.execute.after"]?.(input, output);
31532
+ await frontmatterEnforcer?.["tool.execute.after"]?.(input, output);
31188
31533
  await directoryAgentsInjector?.["tool.execute.after"]?.(input, output);
31189
31534
  await contextWindowMonitor?.["tool.execute.after"]?.(input, output);
31190
31535
  }