thoth-plugin 1.1.2 → 1.2.1

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.
@@ -13,6 +13,8 @@ declare const HooksConfigSchema: z.ZodObject<{
13
13
  "knowledge-persistence": z.ZodOptional<z.ZodBoolean>;
14
14
  "directory-agents-injector": z.ZodOptional<z.ZodBoolean>;
15
15
  "frontmatter-enforcer": z.ZodOptional<z.ZodBoolean>;
16
+ "read-confirmation": z.ZodOptional<z.ZodBoolean>;
17
+ "write-confirmation": z.ZodOptional<z.ZodBoolean>;
16
18
  "todo-continuation": z.ZodOptional<z.ZodBoolean>;
17
19
  "session-recovery": z.ZodOptional<z.ZodBoolean>;
18
20
  "context-window-monitor": z.ZodOptional<z.ZodBoolean>;
@@ -67,6 +69,8 @@ export declare const ThothPluginConfigSchema: z.ZodObject<{
67
69
  "knowledge-persistence": z.ZodOptional<z.ZodBoolean>;
68
70
  "directory-agents-injector": z.ZodOptional<z.ZodBoolean>;
69
71
  "frontmatter-enforcer": z.ZodOptional<z.ZodBoolean>;
72
+ "read-confirmation": z.ZodOptional<z.ZodBoolean>;
73
+ "write-confirmation": z.ZodOptional<z.ZodBoolean>;
70
74
  "todo-continuation": z.ZodOptional<z.ZodBoolean>;
71
75
  "session-recovery": z.ZodOptional<z.ZodBoolean>;
72
76
  "context-window-monitor": z.ZodOptional<z.ZodBoolean>;
@@ -1,6 +1,12 @@
1
1
  ---
2
2
  name: gardener
3
3
  description: Use when knowledge base health needs checking, broken links need fixing, orphan files need registering, or cross-references between related files are missing
4
+ triggers:
5
+ - "Check knowledge base"
6
+ - "Run gardener"
7
+ - "KB health"
8
+ - "Fix broken links"
9
+ - "Check for orphan files"
4
10
  ---
5
11
 
6
12
  # Gardener Skill
@@ -58,12 +64,92 @@ The canonical frontmatter schema is defined in `kernel/config/frontmatter-schema
58
64
 
59
65
  ---
60
66
 
67
+ ## Severity Levels
68
+
69
+ All issues are classified by severity to prioritize repair work:
70
+
71
+ | Severity | Symbol | Meaning | Action |
72
+ |----------|--------|---------|--------|
73
+ | **CRITICAL** | `[C]` | Data integrity at risk, navigation broken | Fix immediately |
74
+ | **ERROR** | `[E]` | Functionality impaired, links broken | Fix soon |
75
+ | **WARNING** | `[W]` | Best practices violated, maintenance debt | Fix when convenient |
76
+ | **INFO** | `[I]` | Suggestions for improvement | Optional |
77
+
78
+ ### Severity by Issue Type
79
+
80
+ | Issue | Default Severity | Escalation Condition |
81
+ |-------|------------------|----------------------|
82
+ | Missing required frontmatter field | ERROR | CRITICAL if `type` missing |
83
+ | Invalid frontmatter value | WARNING | ERROR if `status` or `priority` |
84
+ | Broken internal link | ERROR | CRITICAL if in registry/index |
85
+ | Missing bidirectional link | WARNING | — |
86
+ | Orphan file (not indexed) | WARNING | ERROR if in people/ or projects/ |
87
+ | Registry ghost (indexed but missing) | CRITICAL | — |
88
+ | Stale _index.md (files not listed) | WARNING | ERROR if >5 files missing |
89
+ | Frontmatter schema violation | ERROR | — |
90
+
91
+ ---
92
+
61
93
  ## Mode 1: Health Check
62
94
 
63
- ### Step 1: Run Scan
95
+ ### Step 1: Scan All Categories
96
+
97
+ Perform these checks systematically:
98
+
99
+ #### 1.1 Frontmatter Validation
100
+
101
+ For each `.md` file in the knowledge base:
64
102
 
65
- ```bash
66
- npx tsx scripts/gardener-scan.ts --verbose
103
+ ```
104
+ CHECK: Has frontmatter block (--- ... ---)
105
+ CHECK: Has required fields: type, hemisphere, created, updated
106
+ CHECK: type value is valid (person, project, task, note, reference, etc.)
107
+ CHECK: hemisphere value matches path (work/, life/, coding/, kernel/)
108
+ CHECK: Type-specific required fields present:
109
+ - person: relationship
110
+ - project: status
111
+ - task: status, priority
112
+ CHECK: Values are valid per schema:
113
+ - status (project): planning|active|on-hold|completed|cancelled
114
+ - status (task): pending|in-progress|done|cancelled|blocked
115
+ - priority: P0|P1|P2|P3
116
+ - health: green|yellow|red
117
+ - relationship: manager|peer|report|stakeholder|friend|family
118
+ ```
119
+
120
+ #### 1.2 Link Integrity
121
+
122
+ For each `[[wikilink]]` and `[markdown](link)`:
123
+
124
+ ```
125
+ CHECK: Target file exists
126
+ CHECK: Target path is correct (not moved/renamed)
127
+ CHECK: Bidirectional: if A links to B, does B link to A?
128
+ ```
129
+
130
+ #### 1.3 Index Coverage
131
+
132
+ For each `_index.md` file:
133
+
134
+ ```
135
+ CHECK: All files in same directory are listed
136
+ CHECK: All listed files actually exist (no ghosts)
137
+ CHECK: File summaries are present and accurate
138
+ ```
139
+
140
+ For `registry.md`:
141
+
142
+ ```
143
+ CHECK: All hemispheres represented
144
+ CHECK: Key entity counts are accurate
145
+ CHECK: Last updated date is recent
146
+ ```
147
+
148
+ #### 1.4 Orphan Detection
149
+
150
+ ```
151
+ CHECK: Every .md file (except _index.md, registry.md) is listed in its _index.md
152
+ CHECK: Every entity file has at least one incoming link
67
153
  ```
68
154
 
69
155
  ### Step 2: Synthesize Report
@@ -73,21 +159,34 @@ npx tsx scripts/gardener-scan.ts --verbose
73
159
 
74
160
  **Scanned**: {timestamp}
75
161
  **Total Files**: {count}
76
- **Overall Health**: {healthy|needs-attention|critical}
77
-
78
- ### Summary
79
- | Category | Count | Severity |
80
- |----------|-------|----------|
81
- | Frontmatter Issues | X | warning |
82
- | Broken Links | X | error |
83
- | Orphan Files | X | warning |
84
- | Registry Ghosts | X | error |
85
-
86
- ### Critical Issues (Top 10)
87
- {List with file path and issue}
162
+ **Overall Health**: {HEALTHY|NEEDS-ATTENTION|CRITICAL}
163
+
164
+ ### Summary by Severity
165
+ | Severity | Count | Categories |
166
+ |----------|-------|------------|
167
+ | CRITICAL | X | {list} |
168
+ | ERROR | X | {list} |
169
+ | WARNING | X | {list} |
170
+ | INFO | X | {list} |
171
+
172
+ ### Issue Breakdown
173
+ | Category | [C] | [E] | [W] | [I] |
174
+ |----------|-----|-----|-----|-----|
175
+ | Frontmatter Issues | X | X | X | X |
176
+ | Broken Links | X | X | X | X |
177
+ | Missing Bidirectional | — | — | X | X |
178
+ | Orphan Files | — | X | X | — |
179
+ | Index Staleness | — | X | X | — |
180
+ | Registry Ghosts | X | — | — | — |
181
+
182
+ ### Critical Issues (Must Fix)
183
+ {List all CRITICAL items with file path and specific issue}
184
+
185
+ ### Errors (Should Fix)
186
+ {List top 10 ERROR items}
88
187
 
89
188
  ### Recommendations
90
- {3-5 prioritized actions}
189
+ {3-5 prioritized actions based on findings}
91
190
  ```
92
191
 
93
192
  ---
@@ -228,6 +327,87 @@ For each approved link:
228
327
 
229
328
  ---
230
329
 
330
+ ## Bidirectional Link Verification
331
+
332
+ A healthy knowledge base has bidirectional links: if A references B, B should reference A.
333
+
334
+ ### Detection Algorithm
335
+
336
+ ```
337
+ For each file A:
338
+ For each outgoing link to file B:
339
+ Check if B has any link back to A
340
+ If not: flag as "Missing bidirectional: B should link to A"
341
+ ```
342
+
343
+ ### Severity Classification
344
+
345
+ | Situation | Severity |
346
+ |-----------|----------|
347
+ | Person A mentions Person B, B doesn't mention A | WARNING |
348
+ | Project links to stakeholder, stakeholder doesn't link to project | WARNING |
349
+ | Registry/index links to file, file doesn't link back | INFO (one-way is OK) |
350
+ | Two files in `related:` frontmatter but no body links | WARNING |
351
+
352
+ ### Repair Suggestion Format
353
+
354
+ ```markdown
355
+ ### Missing Bidirectional Links
356
+
357
+ | Source | Target | Evidence | Suggested Fix |
358
+ |--------|--------|----------|---------------|
359
+ | work/people/alice.md | work/people/bob.md | Alice mentions Bob (line 23) | Add `[[alice]]` to bob.md Related section |
360
+ ```
361
+
362
+ ---
363
+
364
+ ## Index Staleness Detection
365
+
366
+ Every directory with content files should have an `_index.md` that lists all files.
367
+
368
+ ### Detection Algorithm
369
+
370
+ ```
371
+ For each directory with _index.md:
372
+ List all .md files in directory (excluding _index.md)
373
+ Parse _index.md for file references
374
+
375
+ STALE if:
376
+ - File exists but not in _index.md (orphan)
377
+ - File in _index.md but doesn't exist (ghost)
378
+ - File count mismatch > 0
379
+ ```
380
+
381
+ ### Severity Classification
382
+
383
+ | Situation | Severity |
384
+ |-----------|----------|
385
+ | 1-2 files missing from _index.md | WARNING |
386
+ | 3-5 files missing from _index.md | WARNING |
387
+ | >5 files missing from _index.md | ERROR |
388
+ | Ghost entry (listed but doesn't exist) | ERROR |
389
+ | _index.md missing entirely in content directory | ERROR |
390
+
391
+ ### Report Format
392
+
393
+ ```markdown
394
+ ### Index Staleness Report
395
+
396
+ | Directory | Files | Indexed | Missing | Ghosts | Severity |
397
+ |-----------|-------|---------|---------|--------|----------|
398
+ | work/people/ | 15 | 12 | 3 | 0 | [W] |
399
+ | work/projects/ | 8 | 8 | 0 | 1 | [E] |
400
+
401
+ #### Missing from Index
402
+ - work/people/new-person.md (created 2026-01-09)
403
+ - work/people/another.md (created 2026-01-08)
404
+
405
+ #### Ghost Entries (file doesn't exist)
406
+ - work/projects/deleted-project.md (remove from _index.md)
407
+ ```
408
+
409
+ ---
410
+
231
411
  ## Red Flags - STOP
232
412
 
233
413
  - About to add 50+ links without review
@@ -280,4 +460,50 @@ Before completing cross-reference mode:
280
460
 
281
461
  ---
282
462
 
283
- *Gardener v3.0 | Part of Thoth Knowledge Management System*
463
+ ## Frontmatter Validation Details
464
+
465
+ ### Required Fields by Type
466
+
467
+ | File Type | Required Fields | Optional Fields |
468
+ |-----------|-----------------|-----------------|
469
+ | **All files** | `type`, `hemisphere`, `created`, `updated` | `tags`, `summary`, `related` |
470
+ | **person** | + `relationship` | `email`, `slack`, `role`, `company` |
471
+ | **project** | + `status` | `priority`, `health`, `due`, `stakeholders` |
472
+ | **task** | + `status`, `priority` | `due`, `project`, `assignee` |
473
+
474
+ ### Validation Error Examples
475
+
476
+ ```markdown
477
+ ### Frontmatter Validation Errors
478
+
479
+ | File | Issue | Severity | Fix |
480
+ |------|-------|----------|-----|
481
+ | work/people/alice.md | Missing `relationship` field | [E] | Add `relationship: peer` |
482
+ | work/projects/foo.md | Invalid status: "wip" | [E] | Change to `status: active` |
483
+ | life/notes/random.md | Missing `type` field | [C] | Add `type: note` |
484
+ | work/people/bob.md | hemisphere: "work" but path is life/ | [W] | Update to `hemisphere: life` |
485
+ ```
486
+
487
+ ### Auto-Fixable Issues
488
+
489
+ The following can be auto-fixed with `/gardener fix`:
490
+
491
+ | Issue | Auto-Fix Action |
492
+ |-------|-----------------|
493
+ | Missing `created` | Set to file creation date |
494
+ | Missing `updated` | Set to file modification date |
495
+ | Missing `hemisphere` | Infer from file path |
496
+ | Incorrect `hemisphere` | Correct to match path |
497
+
498
+ ### Manual-Fix Required
499
+
500
+ | Issue | Why Manual |
501
+ |-------|------------|
502
+ | Missing `type` | Cannot infer content type |
503
+ | Missing `relationship` | Cannot guess relationship |
504
+ | Missing `status` | Cannot guess project/task state |
505
+ | Invalid enum value | Need user to choose correct value |
506
+
507
+ ---
508
+
509
+ *Gardener v4.0 | Part of Thoth Knowledge Management System*
@@ -0,0 +1,207 @@
1
+ ---
2
+ name: onboarding
3
+ description: Structured onboarding for new domains using breadth-before-depth discovery
4
+ triggers:
5
+ - "Let's onboard"
6
+ - "New domain"
7
+ - "Help me set up"
8
+ - "Onboard my"
9
+ - "Learn about my"
10
+ ---
11
+
12
+ # Onboarding Skill
13
+
14
+ You are entering **Onboarding Mode**. Your role is to systematically learn about a new domain of Zeus's life while avoiding the depth trap.
15
+
16
+ ---
17
+
18
+ ## Philosophy
19
+
20
+ 1. **Breadth before depth** — Map the landscape before diving deep
21
+ 2. **Step back pattern** — After 5-10 minutes on any topic, ask "What else?"
22
+ 3. **Prevent premature action** — Focus on understanding, not doing
23
+ 4. **Structured but conversational** — Have a framework, follow Zeus's energy
24
+ 5. **Persist as you go** — Create knowledge files for important entities discovered
25
+
26
+ ---
27
+
28
+ ## Protocol
29
+
30
+ ### Phase 1: Orient (Start Here)
31
+
32
+ Ask these questions before anything else:
33
+
34
+ 1. **Domain**: What domain are we onboarding? (work, life, specific project, specific area)
35
+ 2. **Goal**: What's the goal of this onboarding? (understand context, set up tracking, prepare for something)
36
+ 3. **Sources**: What systems/data sources are available? (email, calendar, documents, nothing yet)
37
+
38
+ **Output**: Clear understanding of scope and available data.
39
+
40
+ ---
41
+
42
+ ### Phase 2: Scan (If Data Sources Available)
43
+
44
+ If Zeus has connected email, calendar, or documents:
45
+
46
+ 1. Fire parallel background agents to scan:
47
+ ```
48
+ background_task(agent="general", prompt="Scan recent emails for people, projects, recurring themes...")
49
+ background_task(agent="general", prompt="Scan calendar for meetings, recurring events, key people...")
50
+ ```
51
+
52
+ 2. Synthesize findings:
53
+ - Key people mentioned
54
+ - Active projects/areas
55
+ - Recurring themes
56
+ - Open items/commitments
57
+
58
+ **Output**: Data-driven overview of the domain landscape.
59
+
60
+ ---
61
+
62
+ ### Phase 3: Discover (Interview Mode)
63
+
64
+ Ask clarifying questions about what you found (or start here if no data sources):
65
+
66
+ | Area | Questions |
67
+ |------|-----------|
68
+ | **People** | Who are the key people? (just names for now — we'll go deeper later) |
69
+ | **Projects/Areas** | What are the main projects or areas of focus? |
70
+ | **Challenges** | What's the biggest challenge or pain point right now? |
71
+ | **Success** | What does success look like? What are you optimizing for? |
72
+ | **Gaps** | What's not working? What falls through the cracks? |
73
+
74
+ **Key rule**: Collect NAMES and TOPICS first. Don't drill into any one area yet.
75
+
76
+ **Output**: List of people, projects, challenges, and goals.
77
+
78
+ ---
79
+
80
+ ### Phase 4: Step Back (CRITICAL)
81
+
82
+ After covering one area for 5-10 minutes, ALWAYS ask one of these:
83
+
84
+ - "We've covered [X]. Before going deeper, are there other areas we should map out?"
85
+ - "What else is on your mind that we haven't touched?"
86
+ - "Is there anything blocking you that we should address first?"
87
+ - "Are there other people or projects I should know about before we continue?"
88
+
89
+ **NEVER** dive deep into one entity without first mapping the full landscape.
90
+
91
+ **Anti-Pattern Detection**: If you've been discussing one project or person for more than 5 minutes and haven't asked about others, STOP and ask.
92
+
93
+ ---
94
+
95
+ ### Phase 5: Deepen (Only After Breadth)
96
+
97
+ Once the landscape is mapped, ask Zeus which area to explore first:
98
+
99
+ - "We've identified [projects A, B, C] and [people X, Y, Z]. Which would you like to detail first?"
100
+ - "What's most urgent or important to capture deeply?"
101
+
102
+ For each entity Zeus prioritizes:
103
+
104
+ 1. Create a knowledge file using appropriate template
105
+ 2. Capture key details, relationships, status
106
+ 3. Add to relevant `_index.md`
107
+ 4. Create bidirectional links to related entities
108
+
109
+ ---
110
+
111
+ ### Phase 6: Close
112
+
113
+ Before ending the onboarding session:
114
+
115
+ 1. **Summarize** what was learned:
116
+ - Domain scope
117
+ - Key entities created
118
+ - Relationships mapped
119
+
120
+ 2. **Identify gaps** to fill later:
121
+ - "We didn't get to [X, Y, Z]. Should we schedule follow-up?"
122
+ - "I'm missing context on [A, B]. Can we cover that next time?"
123
+
124
+ 3. **Set expectations**:
125
+ - "I'll continue learning as we interact. Correct me when I'm wrong."
126
+ - "Mention new people or projects as they come up — I'll capture them."
127
+
128
+ ---
129
+
130
+ ## Anti-Pattern: The Depth Trap
131
+
132
+ ### WRONG
133
+
134
+ ```
135
+ Zeus: "Let's onboard my work"
136
+ Thoth: "Tell me about Project Alpha"
137
+ Zeus: [explains Project Alpha for 20 minutes]
138
+ Thoth: [asks more Project Alpha questions]
139
+ [30 minutes later, deep in Project Alpha details]
140
+ [Never asked about other projects, colleagues, goals]
141
+ ```
142
+
143
+ ### RIGHT
144
+
145
+ ```
146
+ Zeus: "Let's onboard my work"
147
+ Thoth: "Before we dive deep, let me understand the landscape.
148
+ Who are the key people you work with?"
149
+ Zeus: [lists 5 people]
150
+ Thoth: "And what are the main projects or areas you're focused on?"
151
+ Zeus: [lists 3 projects]
152
+ Thoth: "Got it. What's your biggest challenge right now?"
153
+ Zeus: [explains challenge]
154
+ Thoth: "Before we go deeper on any of these, is there anything
155
+ else I should know about — other projects, people, or
156
+ responsibilities?"
157
+ Zeus: [adds 2 more things]
158
+ Thoth: "Now, which of these would you like to detail first?"
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Knowledge Creation During Onboarding
164
+
165
+ As you discover entities, create files:
166
+
167
+ | Entity Type | Template | Location |
168
+ |-------------|----------|----------|
169
+ | Person (work) | `kernel/templates/person.md` | `work/people/` |
170
+ | Person (life) | `kernel/templates/person.md` | `life/people/` |
171
+ | Project | `kernel/templates/project.md` | `{hemisphere}/projects/` |
172
+ | Area/Topic | Create overview file | Appropriate folder |
173
+
174
+ **Rules**:
175
+ - Check if entity already exists before creating (grep for name)
176
+ - Add to `_index.md` immediately after creation
177
+ - Create bidirectional links between related entities
178
+ - Use frontmatter with appropriate type, hemisphere, tags
179
+
180
+ ---
181
+
182
+ ## Verification Checklist
183
+
184
+ Before ending onboarding session:
185
+
186
+ - [ ] Asked about multiple areas (people, projects, challenges, goals)
187
+ - [ ] Used step-back pattern at least once
188
+ - [ ] Didn't spend more than 10 minutes on any single entity before mapping others
189
+ - [ ] Created knowledge files for key entities
190
+ - [ ] Updated relevant `_index.md` files
191
+ - [ ] Identified gaps for follow-up
192
+ - [ ] Zeus knows what to expect going forward
193
+
194
+ ---
195
+
196
+ ## Quick Reference
197
+
198
+ | Trigger | Action |
199
+ |---------|--------|
200
+ | "Let's onboard my work" | Start Phase 1 with work domain |
201
+ | "Help me set up life tracking" | Start Phase 1 with life domain |
202
+ | "New project: X" | Orient on project X specifically |
203
+ | "Learn about my team" | Focus on people discovery |
204
+
205
+ ---
206
+
207
+ *Onboarding Skill v1.0 | Part of Thoth Knowledge Management System*
@@ -3,3 +3,5 @@ export { createTrustLevelTrackerHook, type TrustLevelTrackerHook, type TrustLeve
3
3
  export { createContextApertureHook, type ContextApertureHook, type ContextApertureConfig, } from "./context-aperture";
4
4
  export { createTemporalAwarenessHook, type TemporalAwarenessHook, type TemporalAwarenessConfig, } from "./temporal-awareness";
5
5
  export { createFrontmatterEnforcerHook, type FrontmatterEnforcerHook, type FrontmatterEnforcerConfig, } from "./frontmatter-enforcer";
6
+ export { createReadConfirmationHook, type ReadConfirmationHook, type ReadConfirmationConfig, } from "./read-confirmation";
7
+ export { createWriteConfirmationHook, type WriteConfirmationHook, type WriteConfirmationConfig, } from "./write-confirmation";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Read Confirmation Hook
3
+ *
4
+ * After file reads, injects a confirmation message into the conversation.
5
+ * This creates an audit trail and prevents hallucination about what was read.
6
+ *
7
+ * Value:
8
+ * - Prevents "I read file X" when it wasn't actually read
9
+ * - Creates audit trail of file access
10
+ * - Helps with context management
11
+ */
12
+ export interface ReadConfirmationConfig {
13
+ knowledgeBasePath: string;
14
+ enabled?: boolean;
15
+ /** Only confirm reads within the knowledge base (default: true) */
16
+ kbOnly?: boolean;
17
+ }
18
+ export declare function createReadConfirmationHook(config: ReadConfirmationConfig): {
19
+ "tool.execute.before": (input: {
20
+ tool: string;
21
+ callID: string;
22
+ }, output: {
23
+ args: Record<string, unknown>;
24
+ }) => Promise<void>;
25
+ "tool.execute.after": (input: {
26
+ tool: string;
27
+ callID: string;
28
+ }, output: {
29
+ title: string;
30
+ output: string;
31
+ metadata: unknown;
32
+ }) => Promise<void>;
33
+ } | null;
34
+ export type ReadConfirmationHook = ReturnType<typeof createReadConfirmationHook>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Write Confirmation Hook
3
+ *
4
+ * After file writes/edits, injects a confirmation message with reminders.
5
+ * This creates an audit trail and reinforces the Smart Merge protocol.
6
+ *
7
+ * Value:
8
+ * - Reminds about _index.md updates for new files
9
+ * - Creates audit trail of file modifications
10
+ * - Reinforces Smart Merge protocol
11
+ */
12
+ export interface WriteConfirmationConfig {
13
+ knowledgeBasePath: string;
14
+ enabled?: boolean;
15
+ /** Only confirm writes within the knowledge base (default: true) */
16
+ kbOnly?: boolean;
17
+ }
18
+ export declare function createWriteConfirmationHook(config: WriteConfirmationConfig): {
19
+ "tool.execute.before": (input: {
20
+ tool: string;
21
+ callID: string;
22
+ }, output: {
23
+ args: Record<string, unknown>;
24
+ }) => Promise<void>;
25
+ "tool.execute.after": (input: {
26
+ tool: string;
27
+ callID: string;
28
+ }, output: {
29
+ title: string;
30
+ output: string;
31
+ metadata: unknown;
32
+ }) => Promise<void>;
33
+ } | null;
34
+ export type WriteConfirmationHook = ReturnType<typeof createWriteConfirmationHook>;
package/dist/index.js CHANGED
@@ -3554,6 +3554,91 @@ function createFrontmatterEnforcerHook(config) {
3554
3554
  }
3555
3555
  };
3556
3556
  }
3557
+ // src/hooks/read-confirmation.ts
3558
+ import * as path6 from "path";
3559
+ function createReadConfirmationHook(config) {
3560
+ const { knowledgeBasePath, enabled = true, kbOnly = true } = config;
3561
+ if (!enabled) {
3562
+ return null;
3563
+ }
3564
+ const kbPath = expandPath(knowledgeBasePath);
3565
+ const tracker = {
3566
+ pendingReadPaths: new Map
3567
+ };
3568
+ return {
3569
+ "tool.execute.before": async (input, output) => {
3570
+ if (input.tool !== "read")
3571
+ return;
3572
+ const filePath = output.args?.filePath;
3573
+ if (filePath && input.callID) {
3574
+ tracker.pendingReadPaths.set(input.callID, filePath);
3575
+ }
3576
+ },
3577
+ "tool.execute.after": async (input, output) => {
3578
+ if (input.tool !== "read")
3579
+ return;
3580
+ const filePath = tracker.pendingReadPaths.get(input.callID);
3581
+ tracker.pendingReadPaths.delete(input.callID);
3582
+ if (!filePath)
3583
+ return;
3584
+ if (kbOnly && !filePath.startsWith(kbPath)) {
3585
+ return;
3586
+ }
3587
+ const lineCount = output.output?.split(`
3588
+ `).length || 0;
3589
+ const relativePath = filePath.startsWith(kbPath) ? filePath.slice(kbPath.length + 1) : path6.basename(filePath);
3590
+ log(`[Read confirmed: ${relativePath} (${lineCount} lines)]`);
3591
+ }
3592
+ };
3593
+ }
3594
+ // src/hooks/write-confirmation.ts
3595
+ import * as path7 from "path";
3596
+ function createWriteConfirmationHook(config) {
3597
+ const { knowledgeBasePath, enabled = true, kbOnly = true } = config;
3598
+ if (!enabled) {
3599
+ return null;
3600
+ }
3601
+ const kbPath = expandPath(knowledgeBasePath);
3602
+ const tracker = {
3603
+ pendingWritePaths: new Map
3604
+ };
3605
+ return {
3606
+ "tool.execute.before": async (input, output) => {
3607
+ if (input.tool !== "write" && input.tool !== "edit")
3608
+ return;
3609
+ const filePath = output.args?.filePath;
3610
+ if (filePath && input.callID) {
3611
+ tracker.pendingWritePaths.set(input.callID, {
3612
+ filePath,
3613
+ action: input.tool
3614
+ });
3615
+ }
3616
+ },
3617
+ "tool.execute.after": async (input, output) => {
3618
+ if (input.tool !== "write" && input.tool !== "edit")
3619
+ return;
3620
+ const pending = tracker.pendingWritePaths.get(input.callID);
3621
+ tracker.pendingWritePaths.delete(input.callID);
3622
+ if (!pending)
3623
+ return;
3624
+ const { filePath, action } = pending;
3625
+ if (kbOnly && !filePath.startsWith(kbPath)) {
3626
+ return;
3627
+ }
3628
+ const relativePath = filePath.startsWith(kbPath) ? filePath.slice(kbPath.length + 1) : path7.basename(filePath);
3629
+ const actionLabel = action === "write" ? "Created/Overwrote" : "Edited";
3630
+ const isNewFile = action === "write";
3631
+ const isMarkdownFile = filePath.endsWith(".md");
3632
+ const isIndexFile = relativePath.includes("_index.md") || relativePath.includes("registry.md");
3633
+ let message = `[${actionLabel}: ${relativePath}]`;
3634
+ if (isNewFile && isMarkdownFile && !isIndexFile) {
3635
+ message += `
3636
+ Reminder: Update _index.md if this is a new file. Check bidirectional links.`;
3637
+ }
3638
+ log(message);
3639
+ }
3640
+ };
3641
+ }
3557
3642
  // src/hooks/directory-agents-injector/index.ts
3558
3643
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3559
3644
  import { dirname as dirname4, join as join9, resolve as resolve2 } from "path";
@@ -3678,8 +3763,8 @@ function createDirectoryAgentsInjectorHook(options) {
3678
3763
  }
3679
3764
  if (toInject.length === 0)
3680
3765
  return;
3681
- for (const { path: path6, content } of toInject) {
3682
- const relativePath = path6.replace(knowledgeBasePath, "").replace(/^\//, "");
3766
+ for (const { path: path8, content } of toInject) {
3767
+ const relativePath = path8.replace(knowledgeBasePath, "").replace(/^\//, "");
3683
3768
  output.output += `
3684
3769
 
3685
3770
  [Directory Context: ${relativePath}]
@@ -3766,8 +3851,8 @@ function findNearestMessageWithFields(messageDir) {
3766
3851
  // src/shared-hooks/utils/logger.ts
3767
3852
  import * as fs2 from "fs";
3768
3853
  import * as os2 from "os";
3769
- import * as path6 from "path";
3770
- var logFile = path6.join(os2.tmpdir(), "thoth-plugin.log");
3854
+ import * as path8 from "path";
3855
+ var logFile = path8.join(os2.tmpdir(), "thoth-plugin.log");
3771
3856
  function log2(message, data) {
3772
3857
  try {
3773
3858
  const timestamp = new Date().toISOString();
@@ -5567,10 +5652,10 @@ function mergeDefs(...defs) {
5567
5652
  function cloneDef(schema) {
5568
5653
  return mergeDefs(schema._zod.def);
5569
5654
  }
5570
- function getElementAtPath(obj, path7) {
5571
- if (!path7)
5655
+ function getElementAtPath(obj, path9) {
5656
+ if (!path9)
5572
5657
  return obj;
5573
- return path7.reduce((acc, key) => acc?.[key], obj);
5658
+ return path9.reduce((acc, key) => acc?.[key], obj);
5574
5659
  }
5575
5660
  function promiseAllObject(promisesObj) {
5576
5661
  const keys = Object.keys(promisesObj);
@@ -5929,11 +6014,11 @@ function aborted(x, startIndex = 0) {
5929
6014
  }
5930
6015
  return false;
5931
6016
  }
5932
- function prefixIssues(path7, issues) {
6017
+ function prefixIssues(path9, issues) {
5933
6018
  return issues.map((iss) => {
5934
6019
  var _a;
5935
6020
  (_a = iss).path ?? (_a.path = []);
5936
- iss.path.unshift(path7);
6021
+ iss.path.unshift(path9);
5937
6022
  return iss;
5938
6023
  });
5939
6024
  }
@@ -6101,7 +6186,7 @@ function treeifyError(error, _mapper) {
6101
6186
  return issue2.message;
6102
6187
  };
6103
6188
  const result = { errors: [] };
6104
- const processError = (error2, path7 = []) => {
6189
+ const processError = (error2, path9 = []) => {
6105
6190
  var _a, _b;
6106
6191
  for (const issue2 of error2.issues) {
6107
6192
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -6111,7 +6196,7 @@ function treeifyError(error, _mapper) {
6111
6196
  } else if (issue2.code === "invalid_element") {
6112
6197
  processError({ issues: issue2.issues }, issue2.path);
6113
6198
  } else {
6114
- const fullpath = [...path7, ...issue2.path];
6199
+ const fullpath = [...path9, ...issue2.path];
6115
6200
  if (fullpath.length === 0) {
6116
6201
  result.errors.push(mapper(issue2));
6117
6202
  continue;
@@ -6143,8 +6228,8 @@ function treeifyError(error, _mapper) {
6143
6228
  }
6144
6229
  function toDotPath(_path) {
6145
6230
  const segs = [];
6146
- const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6147
- for (const seg of path7) {
6231
+ const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6232
+ for (const seg of path9) {
6148
6233
  if (typeof seg === "number")
6149
6234
  segs.push(`[${seg}]`);
6150
6235
  else if (typeof seg === "symbol")
@@ -17435,7 +17520,7 @@ Status: ${task.status}`;
17435
17520
  // src/tools/skill/tools.ts
17436
17521
  import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync9, lstatSync, readlinkSync } from "fs";
17437
17522
  import { homedir as homedir5 } from "os";
17438
- import { join as join17, basename as basename3, resolve as resolve3, dirname as dirname5 } from "path";
17523
+ import { join as join17, basename as basename5, resolve as resolve3, dirname as dirname5 } from "path";
17439
17524
  import { fileURLToPath as fileURLToPath2 } from "url";
17440
17525
  var __filename3 = fileURLToPath2(import.meta.url);
17441
17526
  var __dirname3 = dirname5(__filename3);
@@ -17585,7 +17670,7 @@ async function parseSkillMd(skillPath) {
17585
17670
  const { data, body } = parseFrontmatter(content);
17586
17671
  const frontmatter = parseSkillFrontmatter(data);
17587
17672
  const metadata = {
17588
- name: frontmatter.name || basename3(skillPath),
17673
+ name: frontmatter.name || basename5(skillPath),
17589
17674
  description: frontmatter.description,
17590
17675
  license: frontmatter.license,
17591
17676
  allowedTools: frontmatter["allowed-tools"],
@@ -18524,10 +18609,10 @@ function mergeDefs2(...defs) {
18524
18609
  function cloneDef2(schema) {
18525
18610
  return mergeDefs2(schema._zod.def);
18526
18611
  }
18527
- function getElementAtPath2(obj, path7) {
18528
- if (!path7)
18612
+ function getElementAtPath2(obj, path9) {
18613
+ if (!path9)
18529
18614
  return obj;
18530
- return path7.reduce((acc, key) => acc?.[key], obj);
18615
+ return path9.reduce((acc, key) => acc?.[key], obj);
18531
18616
  }
18532
18617
  function promiseAllObject2(promisesObj) {
18533
18618
  const keys = Object.keys(promisesObj);
@@ -18908,11 +18993,11 @@ function aborted2(x, startIndex = 0) {
18908
18993
  }
18909
18994
  return false;
18910
18995
  }
18911
- function prefixIssues2(path7, issues) {
18996
+ function prefixIssues2(path9, issues) {
18912
18997
  return issues.map((iss) => {
18913
18998
  var _a;
18914
18999
  (_a = iss).path ?? (_a.path = []);
18915
- iss.path.unshift(path7);
19000
+ iss.path.unshift(path9);
18916
19001
  return iss;
18917
19002
  });
18918
19003
  }
@@ -19095,7 +19180,7 @@ function formatError2(error45, mapper = (issue3) => issue3.message) {
19095
19180
  }
19096
19181
  function treeifyError2(error45, mapper = (issue3) => issue3.message) {
19097
19182
  const result = { errors: [] };
19098
- const processError = (error46, path7 = []) => {
19183
+ const processError = (error46, path9 = []) => {
19099
19184
  var _a, _b;
19100
19185
  for (const issue3 of error46.issues) {
19101
19186
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -19105,7 +19190,7 @@ function treeifyError2(error45, mapper = (issue3) => issue3.message) {
19105
19190
  } else if (issue3.code === "invalid_element") {
19106
19191
  processError({ issues: issue3.issues }, issue3.path);
19107
19192
  } else {
19108
- const fullpath = [...path7, ...issue3.path];
19193
+ const fullpath = [...path9, ...issue3.path];
19109
19194
  if (fullpath.length === 0) {
19110
19195
  result.errors.push(mapper(issue3));
19111
19196
  continue;
@@ -19137,8 +19222,8 @@ function treeifyError2(error45, mapper = (issue3) => issue3.message) {
19137
19222
  }
19138
19223
  function toDotPath2(_path) {
19139
19224
  const segs = [];
19140
- const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
19141
- for (const seg of path7) {
19225
+ const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
19226
+ for (const seg of path9) {
19142
19227
  if (typeof seg === "number")
19143
19228
  segs.push(`[${seg}]`);
19144
19229
  else if (typeof seg === "symbol")
@@ -30885,13 +30970,13 @@ function resolveRef(ref, ctx) {
30885
30970
  if (!ref.startsWith("#")) {
30886
30971
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
30887
30972
  }
30888
- const path7 = ref.slice(1).split("/").filter(Boolean);
30889
- if (path7.length === 0) {
30973
+ const path9 = ref.slice(1).split("/").filter(Boolean);
30974
+ if (path9.length === 0) {
30890
30975
  return ctx.rootSchema;
30891
30976
  }
30892
30977
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
30893
- if (path7[0] === defsKey) {
30894
- const key = path7[1];
30978
+ if (path9[0] === defsKey) {
30979
+ const key = path9[1];
30895
30980
  if (!key || !ctx.defs[key]) {
30896
30981
  throw new Error(`Reference not found: ${ref}`);
30897
30982
  }
@@ -31306,6 +31391,8 @@ var HooksConfigSchema = exports_external2.object({
31306
31391
  "knowledge-persistence": exports_external2.boolean().optional(),
31307
31392
  "directory-agents-injector": exports_external2.boolean().optional(),
31308
31393
  "frontmatter-enforcer": exports_external2.boolean().optional(),
31394
+ "read-confirmation": exports_external2.boolean().optional(),
31395
+ "write-confirmation": exports_external2.boolean().optional(),
31309
31396
  "todo-continuation": exports_external2.boolean().optional(),
31310
31397
  "session-recovery": exports_external2.boolean().optional(),
31311
31398
  "context-window-monitor": exports_external2.boolean().optional(),
@@ -31343,7 +31430,7 @@ var ThothPluginConfigSchema = exports_external2.object({
31343
31430
  }).strict();
31344
31431
  // src/index.ts
31345
31432
  import * as fs3 from "fs";
31346
- import * as path7 from "path";
31433
+ import * as path9 from "path";
31347
31434
  var sessionSpecializations = new Map;
31348
31435
  function loadConfigFromPath(configPath) {
31349
31436
  try {
@@ -31375,8 +31462,8 @@ function mergeConfigs(base, override) {
31375
31462
  };
31376
31463
  }
31377
31464
  function loadPluginConfig(directory) {
31378
- const userConfigPath = path7.join(getUserConfigDir(), "opencode", "thoth-plugin.json");
31379
- const projectConfigPath = path7.join(directory, ".opencode", "thoth-plugin.json");
31465
+ const userConfigPath = path9.join(getUserConfigDir(), "opencode", "thoth-plugin.json");
31466
+ const projectConfigPath = path9.join(directory, ".opencode", "thoth-plugin.json");
31380
31467
  let config3 = loadConfigFromPath(userConfigPath) ?? {};
31381
31468
  const projectConfig = loadConfigFromPath(projectConfigPath);
31382
31469
  if (projectConfig) {
@@ -31390,15 +31477,15 @@ function resolveKnowledgeBasePath(config3, directory) {
31390
31477
  return expandPath(config3.knowledge_base);
31391
31478
  }
31392
31479
  const commonLocations = [
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")
31480
+ path9.join(process.env.HOME || "", "Repos", "thoth"),
31481
+ path9.join(process.env.HOME || "", "repos", "thoth"),
31482
+ path9.join(process.env.HOME || "", "Projects", "thoth"),
31483
+ path9.join(process.env.HOME || "", "projects", "thoth"),
31484
+ path9.join(process.env.HOME || "", "thoth"),
31485
+ path9.join(directory, "thoth")
31399
31486
  ];
31400
31487
  for (const location of commonLocations) {
31401
- const kernelPath = path7.join(location, "kernel");
31488
+ const kernelPath = path9.join(location, "kernel");
31402
31489
  if (fs3.existsSync(kernelPath)) {
31403
31490
  log(`Found knowledge base at: ${location}`);
31404
31491
  return location;
@@ -31420,6 +31507,8 @@ var ThothPlugin = async (ctx) => {
31420
31507
  const contextAperture = hooksConfig["context-aperture"] !== false ? createContextApertureHook({ knowledgeBasePath }) : null;
31421
31508
  const temporalAwareness = hooksConfig["temporal-awareness"] !== false ? createTemporalAwarenessHook() : null;
31422
31509
  const frontmatterEnforcer = hooksConfig["frontmatter-enforcer"] !== false ? createFrontmatterEnforcerHook({ knowledgeBasePath }) : null;
31510
+ const readConfirmation = hooksConfig["read-confirmation"] !== false ? createReadConfirmationHook({ knowledgeBasePath }) : null;
31511
+ const writeConfirmation = hooksConfig["write-confirmation"] !== false ? createWriteConfirmationHook({ knowledgeBasePath }) : null;
31423
31512
  const todoContinuationEnforcer = hooksConfig["todo-continuation"] !== false ? createTodoContinuationEnforcer(ctx) : null;
31424
31513
  const sessionRecoveryHook = hooksConfig["session-recovery"] !== false ? createSessionRecoveryHook(ctx, { experimental: { auto_resume: true } }) : null;
31425
31514
  const contextWindowMonitor = hooksConfig["context-window-monitor"] !== false ? createContextWindowMonitorHook(ctx) : null;
@@ -31525,11 +31614,15 @@ var ThothPlugin = async (ctx) => {
31525
31614
  await contextAperture?.["tool.execute.before"]?.(input, output);
31526
31615
  await trustLevelTracker?.["tool.execute.before"]?.(input, output);
31527
31616
  await frontmatterEnforcer?.["tool.execute.before"]?.(input, output);
31617
+ await readConfirmation?.["tool.execute.before"]?.(input, output);
31618
+ await writeConfirmation?.["tool.execute.before"]?.(input, output);
31528
31619
  },
31529
31620
  "tool.execute.after": async (input, output) => {
31530
31621
  await trustLevelTracker?.["tool.execute.after"]?.(input, output);
31531
31622
  await contextAperture?.["tool.execute.after"]?.(input, output);
31532
31623
  await frontmatterEnforcer?.["tool.execute.after"]?.(input, output);
31624
+ await readConfirmation?.["tool.execute.after"]?.(input, output);
31625
+ await writeConfirmation?.["tool.execute.after"]?.(input, output);
31533
31626
  await directoryAgentsInjector?.["tool.execute.after"]?.(input, output);
31534
31627
  await contextWindowMonitor?.["tool.execute.after"]?.(input, output);
31535
31628
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thoth-plugin",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "Thoth - Root-level life orchestrator for OpenCode. Unified AI chief of staff combining Sisyphus execution quality, Personal-OS rhythms, and Thoth relationship model.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",