qualitative-research-pro 1.0.0

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.
Files changed (114) hide show
  1. package/AGENTS.md +108 -0
  2. package/CLAUDE.md +171 -0
  3. package/LICENSE +21 -0
  4. package/README.md +166 -0
  5. package/agents/analysis-orchestrator.md +162 -0
  6. package/agents/audit-trail-builder.md +127 -0
  7. package/agents/category-developer.md +179 -0
  8. package/agents/citation-manager.md +83 -0
  9. package/agents/constant-comparator.md +135 -0
  10. package/agents/data-manager.md +104 -0
  11. package/agents/discussion-writer.md +128 -0
  12. package/agents/document-analyst.md +114 -0
  13. package/agents/ethics-reviewer.md +119 -0
  14. package/agents/field-note-analyst.md +124 -0
  15. package/agents/fit-assessor.md +192 -0
  16. package/agents/grounded-theorist.md +210 -0
  17. package/agents/literature-integrator.md +169 -0
  18. package/agents/literature-reviewer.md +112 -0
  19. package/agents/memo-writer.md +234 -0
  20. package/agents/methodology-critic.md +166 -0
  21. package/agents/methods-writer.md +109 -0
  22. package/agents/open-coder.md +187 -0
  23. package/agents/pattern-analyst.md +166 -0
  24. package/agents/peer-reviewer.md +129 -0
  25. package/agents/planner.md +122 -0
  26. package/agents/proposal-writer.md +108 -0
  27. package/agents/reflexivity-auditor.md +128 -0
  28. package/agents/research-designer.md +164 -0
  29. package/agents/research-writer.md +100 -0
  30. package/agents/saturation-assessor.md +159 -0
  31. package/agents/selective-coder.md +167 -0
  32. package/agents/theoretical-coder.md +260 -0
  33. package/agents/theoretical-sampler.md +165 -0
  34. package/agents/transcript-analyst.md +123 -0
  35. package/bin/cli.mjs +236 -0
  36. package/hooks/dist/agent-memory-loader.mjs +94 -0
  37. package/hooks/dist/agent-memory-saver.mjs +113 -0
  38. package/hooks/dist/bash-audit-log.mjs +71 -0
  39. package/hooks/dist/credential-deny.mjs +165 -0
  40. package/hooks/dist/forge-compile-check.mjs +92 -0
  41. package/hooks/dist/gas-snapshot-diff.mjs +71 -0
  42. package/hooks/dist/memory-awareness.mjs +276 -0
  43. package/hooks/dist/natspec-enforcer.mjs +67 -0
  44. package/hooks/dist/passive-learner.mjs +220 -0
  45. package/hooks/dist/pre-compact-continuity.mjs +467 -0
  46. package/hooks/dist/sast-on-edit.mjs +230 -0
  47. package/hooks/dist/session-analytics.mjs +84 -0
  48. package/hooks/dist/session-end-cleanup.mjs +121 -0
  49. package/hooks/dist/session-outcome.mjs +84 -0
  50. package/hooks/dist/session-register.mjs +307 -0
  51. package/hooks/dist/session-start-continuity.mjs +405 -0
  52. package/hooks/dist/slither-on-save.mjs +87 -0
  53. package/hooks/dist/storage-layout-check.mjs +89 -0
  54. package/hooks/dist/transcript-parser.mjs +214 -0
  55. package/install.sh +194 -0
  56. package/package.json +46 -0
  57. package/plugin.json +19 -0
  58. package/rules/academic-writing-style.md +42 -0
  59. package/rules/citation-standards.md +47 -0
  60. package/rules/current-methodological-state.md +40 -0
  61. package/rules/data-handling.md +44 -0
  62. package/rules/finding-output-format.md +47 -0
  63. package/rules/gt-coding-standards.md +40 -0
  64. package/rules/methodological-rigor.md +56 -0
  65. package/rules/quality-criteria.md +41 -0
  66. package/rules/reflexivity-requirements.md +40 -0
  67. package/rules/research-ethics-standards.md +44 -0
  68. package/skills/.gitkeep +2 -0
  69. package/skills/academic-writing/SKILL.md +73 -0
  70. package/skills/action-research/SKILL.md +96 -0
  71. package/skills/apa-formatting/SKILL.md +85 -0
  72. package/skills/case-study-methods/SKILL.md +96 -0
  73. package/skills/category-development/SKILL.md +80 -0
  74. package/skills/chicago-formatting/SKILL.md +81 -0
  75. package/skills/coding-pipeline/SKILL.md +81 -0
  76. package/skills/conceptual-frameworks/SKILL.md +70 -0
  77. package/skills/constant-comparison/SKILL.md +188 -0
  78. package/skills/constructivist-gt/SKILL.md +91 -0
  79. package/skills/data-management-protocols/SKILL.md +67 -0
  80. package/skills/document-analysis/SKILL.md +66 -0
  81. package/skills/ethnographic-methods/SKILL.md +82 -0
  82. package/skills/focus-group-methods/SKILL.md +66 -0
  83. package/skills/formal-theory/SKILL.md +159 -0
  84. package/skills/glaserian-grounded-theory/SKILL.md +212 -0
  85. package/skills/interview-design/SKILL.md +67 -0
  86. package/skills/literature-synthesis/SKILL.md +71 -0
  87. package/skills/member-checking/SKILL.md +66 -0
  88. package/skills/memo-writing/SKILL.md +158 -0
  89. package/skills/mixed-methods-design/SKILL.md +69 -0
  90. package/skills/narrative-inquiry/SKILL.md +101 -0
  91. package/skills/observation-methods/SKILL.md +67 -0
  92. package/skills/open-coding/SKILL.md +176 -0
  93. package/skills/paradigmatic-positioning/SKILL.md +72 -0
  94. package/skills/peer-debriefing/SKILL.md +72 -0
  95. package/skills/phenomenological-methods/SKILL.md +91 -0
  96. package/skills/qualitative-rigor/SKILL.md +78 -0
  97. package/skills/reflexive-practice/SKILL.md +64 -0
  98. package/skills/research-ethics/SKILL.md +64 -0
  99. package/skills/research-proposal-writing/SKILL.md +81 -0
  100. package/skills/research-questions/SKILL.md +66 -0
  101. package/skills/sampling-strategies/SKILL.md +61 -0
  102. package/skills/selective-coding/SKILL.md +183 -0
  103. package/skills/situational-analysis/SKILL.md +93 -0
  104. package/skills/substantive-theory/SKILL.md +169 -0
  105. package/skills/thematic-analysis/SKILL.md +80 -0
  106. package/skills/theoretical-coding/SKILL.md +213 -0
  107. package/skills/theoretical-sampling/SKILL.md +152 -0
  108. package/skills/theoretical-saturation/SKILL.md +179 -0
  109. package/skills/theoretical-sensitivity/SKILL.md +175 -0
  110. package/skills/theory-integration/SKILL.md +85 -0
  111. package/skills/thick-description/SKILL.md +69 -0
  112. package/skills/triangulation/SKILL.md +65 -0
  113. package/skills/visual-modeling/SKILL.md +66 -0
  114. package/skills/vulnerable-populations/SKILL.md +69 -0
@@ -0,0 +1,405 @@
1
+ // src/session-start-continuity.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { execSync } from "child_process";
5
+ function buildHandoffDirName(sessionName, sessionId) {
6
+ const uuidShort = sessionId.replace(/-/g, "").slice(0, 8);
7
+ return `${sessionName}-${uuidShort}`;
8
+ }
9
+ function parseHandoffDirName(dirName) {
10
+ const match = dirName.match(/^(.+)-([0-9a-f]{8})$/i);
11
+ if (match) {
12
+ return { sessionName: match[1], uuidShort: match[2].toLowerCase() };
13
+ }
14
+ return { sessionName: dirName, uuidShort: null };
15
+ }
16
+ function findSessionHandoffWithUUID(sessionName, sessionId) {
17
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
18
+ const handoffsBase = path.join(projectDir, "thoughts", "shared", "handoffs");
19
+ if (!fs.existsSync(handoffsBase)) return null;
20
+ const uuidShort = sessionId.replace(/-/g, "").slice(0, 8).toLowerCase();
21
+ const exactDir = path.join(handoffsBase, `${sessionName}-${uuidShort}`);
22
+ if (fs.existsSync(exactDir)) {
23
+ return findMostRecentMdFile(exactDir);
24
+ }
25
+ const legacyDir = path.join(handoffsBase, sessionName);
26
+ if (fs.existsSync(legacyDir) && fs.statSync(legacyDir).isDirectory()) {
27
+ const result = findMostRecentMdFile(legacyDir);
28
+ if (result) return result;
29
+ }
30
+ const allDirs = fs.readdirSync(handoffsBase).filter((d) => {
31
+ const stat = fs.statSync(path.join(handoffsBase, d));
32
+ if (!stat.isDirectory()) return false;
33
+ const { sessionName: parsedName } = parseHandoffDirName(d);
34
+ return parsedName === sessionName;
35
+ });
36
+ allDirs.sort((a, b) => {
37
+ const statA = fs.statSync(path.join(handoffsBase, a));
38
+ const statB = fs.statSync(path.join(handoffsBase, b));
39
+ return statB.mtime.getTime() - statA.mtime.getTime();
40
+ });
41
+ for (const dir of allDirs) {
42
+ const result = findMostRecentMdFile(path.join(handoffsBase, dir));
43
+ if (result) return result;
44
+ }
45
+ return null;
46
+ }
47
+ function isHandoffFile(filename) {
48
+ return filename.endsWith(".md") || filename.endsWith(".yaml") || filename.endsWith(".yml");
49
+ }
50
+ function findMostRecentMdFile(dirPath) {
51
+ if (!fs.existsSync(dirPath)) return null;
52
+ const handoffFiles = fs.readdirSync(dirPath).filter((f) => isHandoffFile(f)).sort((a, b) => {
53
+ const statA = fs.statSync(path.join(dirPath, a));
54
+ const statB = fs.statSync(path.join(dirPath, b));
55
+ return statB.mtime.getTime() - statA.mtime.getTime();
56
+ });
57
+ return handoffFiles.length > 0 ? path.join(dirPath, handoffFiles[0]) : null;
58
+ }
59
+ function extractYamlFields(content) {
60
+ const goalMatch = content.match(/^goal:\s*(.+)$/m);
61
+ const nowMatch = content.match(/^now:\s*(.+)$/m);
62
+ if (!goalMatch && !nowMatch) return null;
63
+ return {
64
+ goal: goalMatch ? goalMatch[1].trim().replace(/^["']|["']$/g, "") : "",
65
+ now: nowMatch ? nowMatch[1].trim().replace(/^["']|["']$/g, "") : ""
66
+ };
67
+ }
68
+ function extractLedgerSection(handoffContent) {
69
+ const match = handoffContent.match(/(?:^|\n)## Ledger\n([\s\S]*?)(?=\n---\n|\n## [^#]|$)/);
70
+ return match ? `## Ledger
71
+ ${match[1].trim()}` : null;
72
+ }
73
+ function findSessionHandoff(sessionName) {
74
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
75
+ const handoffDir = path.join(projectDir, "thoughts", "shared", "handoffs", sessionName);
76
+ if (!fs.existsSync(handoffDir)) return null;
77
+ const handoffFiles = fs.readdirSync(handoffDir).filter((f) => isHandoffFile(f)).sort((a, b) => {
78
+ const statA = fs.statSync(path.join(handoffDir, a));
79
+ const statB = fs.statSync(path.join(handoffDir, b));
80
+ return statB.mtime.getTime() - statA.mtime.getTime();
81
+ });
82
+ return handoffFiles.length > 0 ? path.join(handoffDir, handoffFiles[0]) : null;
83
+ }
84
+ function pruneLedger(ledgerPath) {
85
+ let content = fs.readFileSync(ledgerPath, "utf-8");
86
+ const originalLength = content.length;
87
+ content = content.replace(/\n### Session Ended \([^)]+\)\n- Reason: \w+\n/g, "");
88
+ const agentReportsMatch = content.match(/## Agent Reports\n([\s\S]*?)(?=\n## |$)/);
89
+ if (agentReportsMatch) {
90
+ const agentReportsSection = agentReportsMatch[0];
91
+ const reports = agentReportsSection.match(/### [^\n]+ \(\d{4}-\d{2}-\d{2}[^)]*\)[\s\S]*?(?=\n### |\n## |$)/g);
92
+ if (reports && reports.length > 10) {
93
+ const keptReports = reports.slice(-10);
94
+ const newAgentReportsSection = "## Agent Reports\n" + keptReports.join("");
95
+ content = content.replace(agentReportsSection, newAgentReportsSection);
96
+ }
97
+ }
98
+ if (content.length !== originalLength) {
99
+ fs.writeFileSync(ledgerPath, content);
100
+ console.error(`Pruned ledger: ${originalLength} \u2192 ${content.length} bytes`);
101
+ }
102
+ }
103
+ function getLatestHandoff(handoffDir) {
104
+ if (!fs.existsSync(handoffDir)) return null;
105
+ const handoffFiles = fs.readdirSync(handoffDir).filter((f) => (f.startsWith("task-") || f.startsWith("auto-handoff-")) && isHandoffFile(f)).sort((a, b) => {
106
+ const statA = fs.statSync(path.join(handoffDir, a));
107
+ const statB = fs.statSync(path.join(handoffDir, b));
108
+ return statB.mtime.getTime() - statA.mtime.getTime();
109
+ });
110
+ if (handoffFiles.length === 0) return null;
111
+ const latestFile = handoffFiles[0];
112
+ const content = fs.readFileSync(path.join(handoffDir, latestFile), "utf-8");
113
+ const isAutoHandoff = latestFile.startsWith("auto-handoff-");
114
+ let taskNumber;
115
+ let status;
116
+ let summary;
117
+ if (isAutoHandoff) {
118
+ const typeMatch = content.match(/type:\s*auto-handoff/i);
119
+ status = typeMatch ? "auto-handoff" : "unknown";
120
+ const timestampMatch = latestFile.match(/auto-handoff-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})/);
121
+ taskNumber = timestampMatch ? timestampMatch[1] : "auto";
122
+ const inProgressMatch = content.match(/## In Progress\n([\s\S]*?)(?=\n## |$)/);
123
+ summary = inProgressMatch ? inProgressMatch[1].trim().split("\n").slice(0, 3).join("; ").substring(0, 150) : "Auto-handoff from pre-compact";
124
+ } else {
125
+ const taskMatch = latestFile.match(/task-(\d+)/);
126
+ taskNumber = taskMatch ? taskMatch[1] : "??";
127
+ const statusMatch = content.match(/status:\s*(success|partial|blocked)/i);
128
+ status = statusMatch ? statusMatch[1] : "unknown";
129
+ const summaryMatch = content.match(/## What Was Done\n([\s\S]*?)(?=\n## |$)/);
130
+ summary = summaryMatch ? summaryMatch[1].trim().split("\n").slice(0, 2).join("; ").substring(0, 150) : "No summary available";
131
+ }
132
+ return {
133
+ filename: latestFile,
134
+ taskNumber,
135
+ status,
136
+ summary,
137
+ isAutoHandoff
138
+ };
139
+ }
140
+ function getUnmarkedHandoffs() {
141
+ try {
142
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
143
+ const dbPath = path.join(projectDir, ".claude", "cache", "artifact-index", "context.db");
144
+ if (!fs.existsSync(dbPath)) {
145
+ return [];
146
+ }
147
+ const result = execSync(
148
+ `sqlite3 "${dbPath}" "SELECT id, session_name, task_number, task_summary FROM handoffs WHERE outcome = 'UNKNOWN' ORDER BY indexed_at DESC LIMIT 5"`,
149
+ { encoding: "utf-8", timeout: 3e3 }
150
+ );
151
+ if (!result.trim()) {
152
+ return [];
153
+ }
154
+ return result.trim().split("\n").map((line) => {
155
+ const [id, session_name, task_number, task_summary] = line.split("|");
156
+ return { id, session_name, task_number: task_number || null, task_summary: task_summary || "" };
157
+ });
158
+ } catch (error) {
159
+ return [];
160
+ }
161
+ }
162
+ async function main() {
163
+ const input = JSON.parse(await readStdin());
164
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
165
+ const sessionType = input.source || input.type;
166
+ let message = "";
167
+ let additionalContext = "";
168
+ let usedHandoffLedger = false;
169
+ const handoffsDir = path.join(projectDir, "thoughts", "shared", "handoffs");
170
+ if (fs.existsSync(handoffsDir)) {
171
+ try {
172
+ const sessionDirs = fs.readdirSync(handoffsDir).filter((d) => {
173
+ const stat = fs.statSync(path.join(handoffsDir, d));
174
+ return stat.isDirectory();
175
+ });
176
+ let mostRecentLedger = null;
177
+ for (const sessionName of sessionDirs) {
178
+ const handoffPath = findSessionHandoff(sessionName);
179
+ if (handoffPath) {
180
+ const content = fs.readFileSync(handoffPath, "utf-8");
181
+ const isYaml = handoffPath.endsWith(".yaml") || handoffPath.endsWith(".yml");
182
+ let goalSummary = "No goal found";
183
+ let currentFocus = "Unknown";
184
+ let ledgerContent = "";
185
+ if (isYaml) {
186
+ const yamlFields = extractYamlFields(content);
187
+ if (yamlFields) {
188
+ goalSummary = yamlFields.goal || "No goal found";
189
+ currentFocus = yamlFields.now || "Unknown";
190
+ ledgerContent = content;
191
+ }
192
+ } else {
193
+ const ledgerSection = extractLedgerSection(content);
194
+ if (ledgerSection) {
195
+ const goalMatch = ledgerSection.match(/\*\*Goal:\*\*\s*([^\n]+)/);
196
+ const nowMatch = ledgerSection.match(/### Now\n\[?-?>?\]?\s*([^\n]+)/);
197
+ goalSummary = goalMatch ? goalMatch[1].trim().substring(0, 100) : "No goal found";
198
+ currentFocus = nowMatch ? nowMatch[1].trim() : "Unknown";
199
+ ledgerContent = ledgerSection;
200
+ }
201
+ }
202
+ if (ledgerContent || isYaml && (goalSummary !== "No goal found" || currentFocus !== "Unknown")) {
203
+ const mtime = fs.statSync(handoffPath).mtime.getTime();
204
+ if (!mostRecentLedger || mtime > mostRecentLedger.mtime) {
205
+ mostRecentLedger = {
206
+ content: ledgerContent || content,
207
+ sessionName,
208
+ handoffPath,
209
+ mtime,
210
+ goalSummary: goalSummary.substring(0, 100),
211
+ currentFocus
212
+ };
213
+ }
214
+ }
215
+ }
216
+ }
217
+ if (mostRecentLedger) {
218
+ usedHandoffLedger = true;
219
+ const { sessionName, goalSummary, currentFocus, content: ledgerSection, handoffPath } = mostRecentLedger;
220
+ const handoffFilename = path.basename(handoffPath);
221
+ if (sessionType === "startup") {
222
+ message = `\u{1F4CB} Handoff Ledger: ${sessionName} \u2192 ${currentFocus} (run /resume_handoff to continue)`;
223
+ } else {
224
+ console.error(`\u2713 Handoff Ledger loaded: ${sessionName} \u2192 ${currentFocus}`);
225
+ message = `[${sessionType}] Loaded from handoff: ${handoffFilename} | Goal: ${goalSummary} | Focus: ${currentFocus}`;
226
+ if (sessionType === "clear" || sessionType === "compact") {
227
+ additionalContext = `Handoff Ledger loaded from ${handoffFilename}:
228
+
229
+ ${ledgerSection}`;
230
+ const unmarkedHandoffs = getUnmarkedHandoffs();
231
+ if (unmarkedHandoffs.length > 0) {
232
+ additionalContext += `
233
+
234
+ ---
235
+
236
+ ## Unmarked Session Outcomes
237
+
238
+ `;
239
+ additionalContext += `The following handoffs have no outcome marked. Consider marking them to improve future session recommendations:
240
+
241
+ `;
242
+ for (const h of unmarkedHandoffs) {
243
+ const taskLabel = h.task_number ? `task-${h.task_number}` : "handoff";
244
+ const summaryPreview = h.task_summary ? h.task_summary.substring(0, 60) + "..." : "(no summary)";
245
+ additionalContext += `- **${h.session_name}/${taskLabel}** (ID: \`${h.id.substring(0, 8)}\`): ${summaryPreview}
246
+ `;
247
+ }
248
+ additionalContext += `
249
+ To mark an outcome:
250
+ \`\`\`bash
251
+ cd ~/.claude && uv run python scripts/core/artifact_mark.py --handoff <ID> --outcome SUCCEEDED|PARTIAL_PLUS|PARTIAL_MINUS|FAILED
252
+ \`\`\`
253
+ `;
254
+ }
255
+ additionalContext += `
256
+
257
+ ---
258
+
259
+ Full handoff available at: ${handoffPath}
260
+ `;
261
+ }
262
+ }
263
+ }
264
+ } catch (error) {
265
+ console.error(`Warning: Error scanning handoffs: ${error}`);
266
+ }
267
+ }
268
+ if (!usedHandoffLedger) {
269
+ const ledgerDir = path.join(projectDir, "thoughts", "ledgers");
270
+ if (!fs.existsSync(ledgerDir)) {
271
+ console.log(JSON.stringify({ result: "continue" }));
272
+ return;
273
+ }
274
+ const ledgerFiles = fs.readdirSync(ledgerDir).filter((f) => f.startsWith("CONTINUITY_CLAUDE-") && f.endsWith(".md")).sort((a, b) => {
275
+ const statA = fs.statSync(path.join(ledgerDir, a));
276
+ const statB = fs.statSync(path.join(ledgerDir, b));
277
+ return statB.mtime.getTime() - statA.mtime.getTime();
278
+ });
279
+ if (ledgerFiles.length > 0) {
280
+ console.error("DEPRECATED: Using legacy ledger file. Migrate to handoff format with /create_handoff");
281
+ const mostRecent = ledgerFiles[0];
282
+ const ledgerPath = path.join(ledgerDir, mostRecent);
283
+ pruneLedger(ledgerPath);
284
+ const ledgerContent = fs.readFileSync(ledgerPath, "utf-8");
285
+ const goalMatch = ledgerContent.match(/## Goal\n([\s\S]*?)(?=\n## |$)/);
286
+ const nowMatch = ledgerContent.match(/- Now: ([^\n]+)/);
287
+ const goalSummary = goalMatch ? goalMatch[1].trim().split("\n")[0].substring(0, 100) : "No goal found";
288
+ const currentFocus = nowMatch ? nowMatch[1].trim() : "Unknown";
289
+ const sessionName = mostRecent.replace("CONTINUITY_CLAUDE-", "").replace(".md", "");
290
+ const handoffDir = path.join(projectDir, "thoughts", "shared", "handoffs", sessionName);
291
+ const latestHandoff = getLatestHandoff(handoffDir);
292
+ if (sessionType === "startup") {
293
+ let startupMsg = `\u{1F4CB} Ledger available: ${sessionName} \u2192 ${currentFocus}`;
294
+ if (latestHandoff) {
295
+ if (latestHandoff.isAutoHandoff) {
296
+ startupMsg += ` | Last handoff: auto (${latestHandoff.status})`;
297
+ } else {
298
+ startupMsg += ` | Last handoff: task-${latestHandoff.taskNumber} (${latestHandoff.status})`;
299
+ }
300
+ }
301
+ startupMsg += " (run /resume_handoff to continue)";
302
+ message = startupMsg;
303
+ } else {
304
+ console.error(`\u2713 Ledger loaded: ${sessionName} \u2192 ${currentFocus}`);
305
+ message = `[${sessionType}] Loaded: ${mostRecent} | Goal: ${goalSummary} | Focus: ${currentFocus}`;
306
+ if (sessionType === "clear" || sessionType === "compact") {
307
+ additionalContext = `Continuity ledger loaded from ${mostRecent}:
308
+
309
+ ${ledgerContent}`;
310
+ const unmarkedHandoffs = getUnmarkedHandoffs();
311
+ if (unmarkedHandoffs.length > 0) {
312
+ additionalContext += `
313
+
314
+ ---
315
+
316
+ ## Unmarked Session Outcomes
317
+
318
+ `;
319
+ additionalContext += `The following handoffs have no outcome marked. Consider marking them to improve future session recommendations:
320
+
321
+ `;
322
+ for (const h of unmarkedHandoffs) {
323
+ const taskLabel = h.task_number ? `task-${h.task_number}` : "handoff";
324
+ const summaryPreview = h.task_summary ? h.task_summary.substring(0, 60) + "..." : "(no summary)";
325
+ additionalContext += `- **${h.session_name}/${taskLabel}** (ID: \`${h.id.substring(0, 8)}\`): ${summaryPreview}
326
+ `;
327
+ }
328
+ additionalContext += `
329
+ To mark an outcome:
330
+ \`\`\`bash
331
+ cd ~/.claude && uv run python scripts/core/artifact_mark.py --handoff <ID> --outcome SUCCEEDED|PARTIAL_PLUS|PARTIAL_MINUS|FAILED
332
+ \`\`\`
333
+ `;
334
+ }
335
+ if (latestHandoff) {
336
+ const handoffPath = path.join(handoffDir, latestHandoff.filename);
337
+ const handoffContent = fs.readFileSync(handoffPath, "utf-8");
338
+ const handoffLabel = latestHandoff.isAutoHandoff ? "Latest auto-handoff" : "Latest task handoff";
339
+ additionalContext += `
340
+
341
+ ---
342
+
343
+ ${handoffLabel} (${latestHandoff.filename}):
344
+ `;
345
+ additionalContext += `Status: ${latestHandoff.status}${latestHandoff.isAutoHandoff ? "" : ` | Task: ${latestHandoff.taskNumber}`}
346
+
347
+ `;
348
+ const truncatedHandoff = handoffContent.length > 2e3 ? handoffContent.substring(0, 2e3) + "\n\n[... truncated, read full file if needed]" : handoffContent;
349
+ additionalContext += truncatedHandoff;
350
+ const allHandoffs = fs.readdirSync(handoffDir).filter((f) => (f.startsWith("task-") || f.startsWith("auto-handoff-")) && isHandoffFile(f)).sort((a, b) => {
351
+ const statA = fs.statSync(path.join(handoffDir, a));
352
+ const statB = fs.statSync(path.join(handoffDir, b));
353
+ return statB.mtime.getTime() - statA.mtime.getTime();
354
+ });
355
+ if (allHandoffs.length > 1) {
356
+ additionalContext += `
357
+
358
+ ---
359
+
360
+ All handoffs in ${handoffDir}:
361
+ `;
362
+ allHandoffs.forEach((f) => {
363
+ additionalContext += `- ${f}
364
+ `;
365
+ });
366
+ }
367
+ }
368
+ }
369
+ }
370
+ } else {
371
+ if (sessionType !== "startup") {
372
+ console.error(`\u26A0 No ledger found. Run /continuity_ledger to track session state.`);
373
+ message = `[${sessionType}] No ledger found. Consider running /continuity_ledger to track session state.`;
374
+ }
375
+ }
376
+ }
377
+ const output = { result: "continue" };
378
+ if (message) {
379
+ output.message = message;
380
+ output.systemMessage = message;
381
+ }
382
+ if (additionalContext) {
383
+ output.hookSpecificOutput = {
384
+ hookEventName: "SessionStart",
385
+ additionalContext
386
+ };
387
+ }
388
+ console.log(JSON.stringify(output));
389
+ }
390
+ async function readStdin() {
391
+ return new Promise((resolve) => {
392
+ let data = "";
393
+ process.stdin.on("data", (chunk) => data += chunk);
394
+ process.stdin.on("end", () => resolve(data));
395
+ });
396
+ }
397
+ main().catch(console.error);
398
+ export {
399
+ buildHandoffDirName,
400
+ extractLedgerSection,
401
+ extractYamlFields,
402
+ findSessionHandoff,
403
+ findSessionHandoffWithUUID,
404
+ parseHandoffDirName
405
+ };
@@ -0,0 +1,87 @@
1
+ // src/slither-on-save.ts
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { execSync } from "child_process";
4
+ import { join } from "path";
5
+ function readStdin() {
6
+ return readFileSync(0, "utf-8");
7
+ }
8
+ function isSlitherAvailable() {
9
+ try {
10
+ execSync("which slither", { encoding: "utf-8", timeout: 5e3 });
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+ function findProjectRoot(cwd) {
17
+ let dir = cwd;
18
+ while (dir !== "/") {
19
+ if (existsSync(join(dir, "foundry.toml"))) return dir;
20
+ const parent = join(dir, "..");
21
+ if (parent === dir) break;
22
+ dir = parent;
23
+ }
24
+ return null;
25
+ }
26
+ function runSlither(cwd) {
27
+ const projectRoot = findProjectRoot(cwd);
28
+ if (!projectRoot) {
29
+ return { success: true, output: "", findings: 0 };
30
+ }
31
+ try {
32
+ const output = execSync(
33
+ 'slither . --filter-paths "test/,script/,lib/" --json - 2>/dev/null',
34
+ {
35
+ encoding: "utf-8",
36
+ timeout: 12e4,
37
+ maxBuffer: 2 * 1024 * 1024,
38
+ cwd: projectRoot
39
+ }
40
+ );
41
+ try {
42
+ const parsed = JSON.parse(output);
43
+ const detectors = parsed.results?.detectors || [];
44
+ const highMed = detectors.filter(
45
+ (d) => d.impact === "High" || d.impact === "Medium"
46
+ );
47
+ return { success: true, output: JSON.stringify(highMed, null, 2), findings: highMed.length };
48
+ } catch {
49
+ return { success: true, output, findings: 0 };
50
+ }
51
+ } catch (error) {
52
+ return { success: false, output: error.message, findings: 0 };
53
+ }
54
+ }
55
+ async function main() {
56
+ const input = JSON.parse(readStdin());
57
+ if (input.tool_name !== "Write" && input.tool_name !== "Edit") {
58
+ console.log("{}");
59
+ return;
60
+ }
61
+ const filePath = input.tool_input?.file_path || input.tool_response?.filePath || "";
62
+ if (!filePath.endsWith(".sol")) {
63
+ console.log("{}");
64
+ return;
65
+ }
66
+ if (!isSlitherAvailable()) {
67
+ console.log("{}");
68
+ return;
69
+ }
70
+ const result = runSlither(input.cwd);
71
+ if (result.findings > 0) {
72
+ console.log(JSON.stringify({
73
+ hookSpecificOutput: {
74
+ hookEventName: "PostToolUse",
75
+ additionalContext: `SLITHER: ${result.findings} High/Medium finding(s) detected. Review before proceeding.
76
+
77
+ ${result.output.slice(0, 2e3)}`
78
+ }
79
+ }));
80
+ } else {
81
+ console.log("{}");
82
+ }
83
+ }
84
+ main().catch((err) => {
85
+ console.error(err.message);
86
+ process.exit(1);
87
+ });
@@ -0,0 +1,89 @@
1
+ // src/storage-layout-check.ts
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { execSync } from "child_process";
4
+ import { join } from "path";
5
+ function readStdin() {
6
+ return readFileSync(0, "utf-8");
7
+ }
8
+ function isUpgradeable(filePath) {
9
+ if (!existsSync(filePath)) return false;
10
+ const content = readFileSync(filePath, "utf-8");
11
+ return content.includes("Initializable") || content.includes("UUPSUpgradeable") || content.includes("TransparentUpgradeableProxy") || content.includes("initializer") || content.includes("__gap");
12
+ }
13
+ function findProjectRoot(cwd) {
14
+ let dir = cwd;
15
+ while (dir !== "/") {
16
+ if (existsSync(join(dir, "foundry.toml"))) return dir;
17
+ const parent = join(dir, "..");
18
+ if (parent === dir) break;
19
+ dir = parent;
20
+ }
21
+ return null;
22
+ }
23
+ function getContractName(filePath) {
24
+ if (!existsSync(filePath)) return null;
25
+ const content = readFileSync(filePath, "utf-8");
26
+ const match = content.match(/contract\s+(\w+)/);
27
+ return match ? match[1] : null;
28
+ }
29
+ async function main() {
30
+ const input = JSON.parse(readStdin());
31
+ if (input.tool_name !== "Write" && input.tool_name !== "Edit") {
32
+ console.log("{}");
33
+ return;
34
+ }
35
+ const filePath = input.tool_input?.file_path || input.tool_response?.filePath || "";
36
+ if (!filePath.endsWith(".sol")) {
37
+ console.log("{}");
38
+ return;
39
+ }
40
+ if (!isUpgradeable(filePath)) {
41
+ console.log("{}");
42
+ return;
43
+ }
44
+ const contractName = getContractName(filePath);
45
+ if (!contractName) {
46
+ console.log("{}");
47
+ return;
48
+ }
49
+ const projectRoot = findProjectRoot(input.cwd);
50
+ if (!projectRoot) {
51
+ console.log("{}");
52
+ return;
53
+ }
54
+ try {
55
+ execSync(`forge build 2>/dev/null`, {
56
+ cwd: projectRoot,
57
+ timeout: 3e4
58
+ });
59
+ const layout = execSync(
60
+ `forge inspect ${contractName} storage-layout 2>/dev/null`,
61
+ {
62
+ encoding: "utf-8",
63
+ timeout: 15e3,
64
+ cwd: projectRoot
65
+ }
66
+ );
67
+ const hasGap = layout.includes("__gap");
68
+ console.log(JSON.stringify({
69
+ hookSpecificOutput: {
70
+ hookEventName: "PostToolUse",
71
+ additionalContext: `UPGRADEABLE CONTRACT: ${contractName}
72
+ ${hasGap ? "Storage gap detected." : "WARNING: No __gap found. Add uint256[50] private __gap for future upgrade safety."}
73
+ Verify storage layout compatibility: forge inspect ${contractName} storage-layout`
74
+ }
75
+ }));
76
+ } catch {
77
+ console.log(JSON.stringify({
78
+ hookSpecificOutput: {
79
+ hookEventName: "PostToolUse",
80
+ additionalContext: `UPGRADEABLE CONTRACT: ${contractName}
81
+ Could not inspect storage layout. Run: forge inspect ${contractName} storage-layout`
82
+ }
83
+ }));
84
+ }
85
+ }
86
+ main().catch((err) => {
87
+ console.error(err.message);
88
+ process.exit(1);
89
+ });