@rkarim08/sia 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.
- package/.claude-plugin/marketplace.json +35 -0
- package/.claude-plugin/plugin.json +27 -0
- package/.mcp.json +13 -0
- package/CLAUDE.md +226 -0
- package/LICENSE +202 -0
- package/PLUGIN_README.md +253 -0
- package/README.md +1013 -0
- package/agents/sia-changelog-writer.md +89 -0
- package/agents/sia-code-reviewer.md +86 -0
- package/agents/sia-conflict-resolver.md +100 -0
- package/agents/sia-convention-enforcer.md +69 -0
- package/agents/sia-debug.md +106 -0
- package/agents/sia-decision-reviewer.md +101 -0
- package/agents/sia-dependency-tracker.md +80 -0
- package/agents/sia-explain.md +126 -0
- package/agents/sia-feature.md +116 -0
- package/agents/sia-knowledge-capture.md +117 -0
- package/agents/sia-lead-architecture-advisor.md +93 -0
- package/agents/sia-lead-team-health.md +107 -0
- package/agents/sia-migration.md +100 -0
- package/agents/sia-onboarding.md +115 -0
- package/agents/sia-orientation.md +99 -0
- package/agents/sia-pm-briefing.md +106 -0
- package/agents/sia-pm-risk-advisor.md +82 -0
- package/agents/sia-qa-analyst.md +116 -0
- package/agents/sia-qa-regression-map.md +94 -0
- package/agents/sia-refactor.md +115 -0
- package/agents/sia-regression.md +112 -0
- package/agents/sia-security-audit.md +125 -0
- package/agents/sia-test-advisor.md +91 -0
- package/hooks/hooks.json +98 -0
- package/migrations/bridge/001_initial.sql +34 -0
- package/migrations/episodic/001_initial.sql +35 -0
- package/migrations/meta/001_initial.sql +68 -0
- package/migrations/semantic/001_initial.sql +292 -0
- package/migrations/semantic/002_ontology.sql +89 -0
- package/migrations/semantic/003_freshness.sql +63 -0
- package/migrations/semantic/004_v5_unified_schema.sql +194 -0
- package/migrations/semantic/005_backfill_event_kinds.sql +8 -0
- package/migrations/semantic/006_tree_sitter.sql +6 -0
- package/migrations/semantic/007_branch_snapshots.sql +22 -0
- package/package.json +110 -0
- package/scripts/branch-switch.sh +13 -0
- package/scripts/build-wasm-grammars.sh +81 -0
- package/scripts/post-compact.sh +8 -0
- package/scripts/post-tool-use.sh +10 -0
- package/scripts/pre-compact.sh +8 -0
- package/scripts/session-end.sh +8 -0
- package/scripts/session-start.sh +8 -0
- package/scripts/start-mcp.ts +45 -0
- package/scripts/stop-hook.sh +8 -0
- package/scripts/user-prompt-submit.sh +8 -0
- package/scripts/viz-server.ts +152 -0
- package/skills/sia-brainstorm/SKILL.md +156 -0
- package/skills/sia-brainstorm/scripts/frame-template.html +214 -0
- package/skills/sia-brainstorm/scripts/helper.js +95 -0
- package/skills/sia-brainstorm/scripts/server.cjs +338 -0
- package/skills/sia-brainstorm/scripts/start-server.sh +153 -0
- package/skills/sia-brainstorm/scripts/stop-server.sh +55 -0
- package/skills/sia-brainstorm/spec-document-reviewer-prompt.md +49 -0
- package/skills/sia-brainstorm/visual-companion.md +286 -0
- package/skills/sia-capture/SKILL.md +64 -0
- package/skills/sia-compare/SKILL.md +33 -0
- package/skills/sia-conflicts/SKILL.md +38 -0
- package/skills/sia-debug-workflow/SKILL.md +120 -0
- package/skills/sia-debug-workflow/root-cause-tracing.md +70 -0
- package/skills/sia-debug-workflow/scripts/find-polluter.sh +64 -0
- package/skills/sia-debug-workflow/temporal-investigation.md +72 -0
- package/skills/sia-digest/SKILL.md +23 -0
- package/skills/sia-dispatch/SKILL.md +69 -0
- package/skills/sia-dispatch/agent-task-template.md +99 -0
- package/skills/sia-doctor/SKILL.md +39 -0
- package/skills/sia-execute/SKILL.md +70 -0
- package/skills/sia-execute-plan/SKILL.md +85 -0
- package/skills/sia-export-import/SKILL.md +49 -0
- package/skills/sia-export-knowledge/SKILL.md +46 -0
- package/skills/sia-finish/SKILL.md +100 -0
- package/skills/sia-finish/pr-summary-template.md +54 -0
- package/skills/sia-freshness/SKILL.md +38 -0
- package/skills/sia-history/SKILL.md +42 -0
- package/skills/sia-impact/SKILL.md +70 -0
- package/skills/sia-index/SKILL.md +54 -0
- package/skills/sia-install/SKILL.md +39 -0
- package/skills/sia-lead-compliance/SKILL.md +16 -0
- package/skills/sia-lead-drift-report/SKILL.md +16 -0
- package/skills/sia-lead-knowledge-map/SKILL.md +16 -0
- package/skills/sia-learn/SKILL.md +58 -0
- package/skills/sia-plan/SKILL.md +68 -0
- package/skills/sia-plan/plan-reviewer-prompt.md +63 -0
- package/skills/sia-playbooks/SKILL.md +29 -0
- package/skills/sia-playbooks/reference-feature.md +100 -0
- package/skills/sia-playbooks/reference-flagging.md +50 -0
- package/skills/sia-playbooks/reference-orientation.md +92 -0
- package/skills/sia-playbooks/reference-regression.md +115 -0
- package/skills/sia-playbooks/reference-review.md +64 -0
- package/skills/sia-playbooks/reference-tools.md +239 -0
- package/skills/sia-pm-decision-log/SKILL.md +28 -0
- package/skills/sia-pm-risk-dashboard/SKILL.md +24 -0
- package/skills/sia-pm-sprint-summary/SKILL.md +27 -0
- package/skills/sia-prune/SKILL.md +45 -0
- package/skills/sia-qa-coverage/SKILL.md +28 -0
- package/skills/sia-qa-flaky/SKILL.md +20 -0
- package/skills/sia-qa-report/SKILL.md +26 -0
- package/skills/sia-reindex/SKILL.md +30 -0
- package/skills/sia-review-respond/SKILL.md +88 -0
- package/skills/sia-review-respond/pushback-patterns.md +90 -0
- package/skills/sia-search/SKILL.md +47 -0
- package/skills/sia-setup/SKILL.md +82 -0
- package/skills/sia-setup/setup-checklist.md +97 -0
- package/skills/sia-stats/SKILL.md +36 -0
- package/skills/sia-status/SKILL.md +44 -0
- package/skills/sia-sync/SKILL.md +46 -0
- package/skills/sia-team/SKILL.md +64 -0
- package/skills/sia-test/SKILL.md +92 -0
- package/skills/sia-test/testing-anti-patterns.md +104 -0
- package/skills/sia-tour/SKILL.md +29 -0
- package/skills/sia-upgrade/SKILL.md +43 -0
- package/skills/sia-verify/SKILL.md +81 -0
- package/skills/sia-visualize/SKILL.md +28 -0
- package/skills/sia-visualize-live/SKILL.md +55 -0
- package/skills/sia-visualize-live/scripts/graph-template.html +389 -0
- package/skills/sia-visualize-live/scripts/start-visualizer.sh +161 -0
- package/skills/sia-visualize-live/scripts/stop-visualizer.sh +55 -0
- package/skills/sia-visualize-live/scripts/visualizer-server.cjs +264 -0
- package/skills/sia-workspace/SKILL.md +57 -0
- package/src/agent/claude-md-template-flagging.md +219 -0
- package/src/agent/claude-md-template.md +213 -0
- package/src/agent/modules/sia-feature.md +100 -0
- package/src/agent/modules/sia-flagging.md +50 -0
- package/src/agent/modules/sia-orientation.md +92 -0
- package/src/agent/modules/sia-regression.md +115 -0
- package/src/agent/modules/sia-review.md +64 -0
- package/src/agent/modules/sia-tools.md +239 -0
- package/src/ast/extractors/c-include.ts +189 -0
- package/src/ast/extractors/csharp-project.ts +260 -0
- package/src/ast/extractors/prisma-schema.ts +44 -0
- package/src/ast/extractors/project-manifest.ts +111 -0
- package/src/ast/extractors/sql-schema.ts +67 -0
- package/src/ast/extractors/tier-a.ts +423 -0
- package/src/ast/extractors/tier-b.ts +289 -0
- package/src/ast/extractors/tier-dispatch.ts +247 -0
- package/src/ast/index-worker.ts +108 -0
- package/src/ast/indexer.ts +484 -0
- package/src/ast/languages.ts +408 -0
- package/src/ast/pagerank-builder.ts +125 -0
- package/src/ast/path-utils.ts +137 -0
- package/src/ast/tree-sitter/backends/native.ts +57 -0
- package/src/ast/tree-sitter/backends/wasm.ts +39 -0
- package/src/ast/tree-sitter/call-walker.ts +44 -0
- package/src/ast/tree-sitter/edit-computer.ts +55 -0
- package/src/ast/tree-sitter/query-runner.ts +46 -0
- package/src/ast/tree-sitter/service.ts +174 -0
- package/src/ast/tree-sitter/tree-cache.ts +39 -0
- package/src/ast/tree-sitter/types.ts +79 -0
- package/src/ast/watcher.ts +322 -0
- package/src/capture/chunker.ts +169 -0
- package/src/capture/consolidate.ts +127 -0
- package/src/capture/edge-inferrer.ts +161 -0
- package/src/capture/embedder.ts +166 -0
- package/src/capture/embedding-cache.ts +73 -0
- package/src/capture/flag-processor.ts +64 -0
- package/src/capture/hook.ts +67 -0
- package/src/capture/pipeline.ts +450 -0
- package/src/capture/prompts/consolidate.ts +25 -0
- package/src/capture/prompts/edge-infer.ts +29 -0
- package/src/capture/prompts/extract-flagged.ts +36 -0
- package/src/capture/prompts/extract.ts +42 -0
- package/src/capture/tokenizer.ts +147 -0
- package/src/capture/track-a-ast.ts +93 -0
- package/src/capture/track-b-llm.ts +149 -0
- package/src/capture/types.ts +64 -0
- package/src/cli/commands/community.ts +137 -0
- package/src/cli/commands/compare.ts +123 -0
- package/src/cli/commands/conflicts.ts +41 -0
- package/src/cli/commands/digest.ts +197 -0
- package/src/cli/commands/disable-flagging.ts +34 -0
- package/src/cli/commands/doctor.ts +240 -0
- package/src/cli/commands/download-model.ts +161 -0
- package/src/cli/commands/enable-flagging.ts +34 -0
- package/src/cli/commands/export-knowledge.ts +208 -0
- package/src/cli/commands/export.ts +85 -0
- package/src/cli/commands/freshness.ts +164 -0
- package/src/cli/commands/graph.ts +51 -0
- package/src/cli/commands/history.ts +139 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/install.ts +156 -0
- package/src/cli/commands/lead-report.ts +241 -0
- package/src/cli/commands/learn.ts +321 -0
- package/src/cli/commands/pm-report.ts +413 -0
- package/src/cli/commands/prune.ts +75 -0
- package/src/cli/commands/qa-report.ts +278 -0
- package/src/cli/commands/reindex.ts +104 -0
- package/src/cli/commands/rollback.ts +70 -0
- package/src/cli/commands/search.ts +103 -0
- package/src/cli/commands/server.ts +91 -0
- package/src/cli/commands/share.ts +33 -0
- package/src/cli/commands/stats.ts +79 -0
- package/src/cli/commands/status.ts +176 -0
- package/src/cli/commands/sync.ts +96 -0
- package/src/cli/commands/team.ts +118 -0
- package/src/cli/commands/tour.ts +157 -0
- package/src/cli/commands/visualize-live.ts +162 -0
- package/src/cli/commands/workspace.ts +117 -0
- package/src/cli/index.ts +424 -0
- package/src/cli/learn-progress.ts +87 -0
- package/src/community/detection-bridge.ts +344 -0
- package/src/community/leiden.ts +462 -0
- package/src/community/raptor.ts +210 -0
- package/src/community/scheduler.ts +74 -0
- package/src/community/summarize.ts +115 -0
- package/src/decay/archiver.ts +73 -0
- package/src/decay/bridge-orphan-cleanup.ts +212 -0
- package/src/decay/consolidation-sweep.ts +112 -0
- package/src/decay/decay.ts +116 -0
- package/src/decay/deep-validator.ts +62 -0
- package/src/decay/episodic-promoter.ts +132 -0
- package/src/decay/maintenance-scheduler.ts +326 -0
- package/src/decay/scheduler.ts +6 -0
- package/src/decay/session-sweeper.ts +79 -0
- package/src/decay/types.ts +17 -0
- package/src/freshness/confidence-decay.ts +122 -0
- package/src/freshness/cuckoo-filter.ts +176 -0
- package/src/freshness/deep-validation.ts +345 -0
- package/src/freshness/dirty-tracker.ts +237 -0
- package/src/freshness/file-watcher-layer.ts +119 -0
- package/src/freshness/firewall.ts +64 -0
- package/src/freshness/git-reconcile-layer.ts +161 -0
- package/src/freshness/inverted-index.ts +158 -0
- package/src/freshness/stale-read-layer.ts +222 -0
- package/src/graph/audit.ts +69 -0
- package/src/graph/bridge-db.ts +141 -0
- package/src/graph/communities.ts +195 -0
- package/src/graph/db-interface.ts +259 -0
- package/src/graph/edges.ts +163 -0
- package/src/graph/entities.ts +327 -0
- package/src/graph/episodic-db.ts +113 -0
- package/src/graph/flags.ts +31 -0
- package/src/graph/meta-db.ts +200 -0
- package/src/graph/semantic-db.ts +101 -0
- package/src/graph/session-resume.ts +56 -0
- package/src/graph/snapshots.ts +342 -0
- package/src/graph/staging.ts +151 -0
- package/src/graph/types.ts +128 -0
- package/src/hooks/adapters/claude-code.ts +21 -0
- package/src/hooks/adapters/cline.ts +43 -0
- package/src/hooks/adapters/cursor.ts +65 -0
- package/src/hooks/adapters/generic.ts +12 -0
- package/src/hooks/agent-detect.ts +34 -0
- package/src/hooks/claude-md-directives.ts +32 -0
- package/src/hooks/event-router.ts +182 -0
- package/src/hooks/extractors/pattern-detector.ts +111 -0
- package/src/hooks/handlers/post-compact.ts +30 -0
- package/src/hooks/handlers/post-tool-use.ts +403 -0
- package/src/hooks/handlers/pre-compact.ts +100 -0
- package/src/hooks/handlers/session-end.ts +47 -0
- package/src/hooks/handlers/session-start.ts +154 -0
- package/src/hooks/handlers/stop.ts +128 -0
- package/src/hooks/handlers/user-prompt-submit.ts +68 -0
- package/src/hooks/plugin-branch-switch.ts +68 -0
- package/src/hooks/plugin-common.ts +47 -0
- package/src/hooks/plugin-post-compact.ts +28 -0
- package/src/hooks/plugin-post-tool-use.ts +38 -0
- package/src/hooks/plugin-pre-compact.ts +37 -0
- package/src/hooks/plugin-session-end.ts +37 -0
- package/src/hooks/plugin-session-start.ts +75 -0
- package/src/hooks/plugin-stop.ts +61 -0
- package/src/hooks/plugin-user-prompt-submit.ts +47 -0
- package/src/hooks/types.ts +43 -0
- package/src/knowledge/discovery.ts +238 -0
- package/src/knowledge/external-refs.ts +98 -0
- package/src/knowledge/freshness.ts +221 -0
- package/src/knowledge/ingest.ts +330 -0
- package/src/knowledge/markdown-export.ts +229 -0
- package/src/knowledge/markdown-import.ts +359 -0
- package/src/knowledge/patterns.ts +74 -0
- package/src/knowledge/templates.ts +307 -0
- package/src/llm/ai-sdk-adapter.ts +46 -0
- package/src/llm/config.ts +88 -0
- package/src/llm/cost-tracker.ts +110 -0
- package/src/llm/prompts/extraction.ts +55 -0
- package/src/llm/prompts/summarization.ts +36 -0
- package/src/llm/prompts/validation.ts +37 -0
- package/src/llm/provider-registry.ts +68 -0
- package/src/llm/reliability.ts +179 -0
- package/src/llm/schemas.ts +52 -0
- package/src/mcp/freshness-annotator.ts +69 -0
- package/src/mcp/server.ts +949 -0
- package/src/mcp/tools/sia-ast-query.ts +225 -0
- package/src/mcp/tools/sia-at-time.ts +151 -0
- package/src/mcp/tools/sia-backlinks.ts +87 -0
- package/src/mcp/tools/sia-batch-execute.ts +169 -0
- package/src/mcp/tools/sia-by-file.ts +89 -0
- package/src/mcp/tools/sia-community.ts +113 -0
- package/src/mcp/tools/sia-doctor.ts +73 -0
- package/src/mcp/tools/sia-execute-file.ts +122 -0
- package/src/mcp/tools/sia-execute.ts +104 -0
- package/src/mcp/tools/sia-expand.ts +158 -0
- package/src/mcp/tools/sia-fetch-and-index.ts +241 -0
- package/src/mcp/tools/sia-flag.ts +65 -0
- package/src/mcp/tools/sia-index.ts +111 -0
- package/src/mcp/tools/sia-note.ts +134 -0
- package/src/mcp/tools/sia-search.ts +105 -0
- package/src/mcp/tools/sia-stats.ts +63 -0
- package/src/mcp/tools/sia-sync-status.ts +44 -0
- package/src/mcp/tools/sia-upgrade.ts +247 -0
- package/src/mcp/truncate.ts +231 -0
- package/src/native/bridge.ts +167 -0
- package/src/native/fallback-ast-diff.ts +144 -0
- package/src/native/fallback-graph.ts +325 -0
- package/src/ontology/constraints.ts +56 -0
- package/src/ontology/errors.ts +8 -0
- package/src/ontology/middleware.ts +266 -0
- package/src/retrieval/bm25-search.ts +151 -0
- package/src/retrieval/context-assembly.ts +76 -0
- package/src/retrieval/graph-traversal.ts +168 -0
- package/src/retrieval/pagerank.ts +40 -0
- package/src/retrieval/query-classifier.ts +106 -0
- package/src/retrieval/reranker.ts +156 -0
- package/src/retrieval/search.ts +236 -0
- package/src/retrieval/throttle.ts +102 -0
- package/src/retrieval/vector-search.ts +203 -0
- package/src/retrieval/workspace-search.ts +130 -0
- package/src/sandbox/context-mode.ts +285 -0
- package/src/sandbox/credential-pass.ts +55 -0
- package/src/sandbox/executor.ts +235 -0
- package/src/security/pattern-detector.ts +127 -0
- package/src/security/rule-of-two.ts +50 -0
- package/src/security/sanitize.ts +46 -0
- package/src/security/semantic-consistency.ts +93 -0
- package/src/security/staging-promoter.ts +154 -0
- package/src/shared/config.ts +302 -0
- package/src/shared/diagnostics.ts +210 -0
- package/src/shared/errors.ts +48 -0
- package/src/shared/git-utils.ts +143 -0
- package/src/shared/llm-client.ts +120 -0
- package/src/shared/logger.ts +99 -0
- package/src/shared/types.ts +79 -0
- package/src/sync/client.ts +43 -0
- package/src/sync/conflict.ts +106 -0
- package/src/sync/dedup.ts +183 -0
- package/src/sync/hlc.ts +117 -0
- package/src/sync/keychain.ts +144 -0
- package/src/sync/pull.ts +232 -0
- package/src/sync/push.ts +131 -0
- package/src/types/chokidar.d.ts +23 -0
- package/src/visualization/graph-renderer.ts +312 -0
- package/src/visualization/subgraph-extract.ts +208 -0
- package/src/visualization/views/community-clusters.ts +246 -0
- package/src/visualization/views/dependency-map.ts +189 -0
- package/src/visualization/views/graph-explorer.ts +364 -0
- package/src/visualization/views/timeline.ts +247 -0
- package/src/workspace/api-contracts.ts +226 -0
- package/src/workspace/cross-repo.ts +61 -0
- package/src/workspace/detector.ts +190 -0
- package/src/workspace/manifest.ts +141 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// Module: post-tool-use — PostToolUse hook handler
|
|
2
|
+
//
|
|
3
|
+
// Receives events after each tool invocation and extracts knowledge:
|
|
4
|
+
// - Write → FileNode entity + TrackA code entities + knowledge patterns
|
|
5
|
+
// - Edit → EditEvent entity
|
|
6
|
+
// - Bash → ExecutionEvent entity + git commit detection + error detection
|
|
7
|
+
// - Read → touch for importance (no new entities)
|
|
8
|
+
|
|
9
|
+
import { extractTrackA } from "@/capture/track-a-ast";
|
|
10
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
11
|
+
import { insertEntity } from "@/graph/entities";
|
|
12
|
+
import { detectCommitPatterns, detectKnowledgePatterns } from "@/hooks/extractors/pattern-detector";
|
|
13
|
+
import type { HookEvent, HookHandler, HookResponse } from "@/hooks/types";
|
|
14
|
+
|
|
15
|
+
/** Extract the basename from a file path. */
|
|
16
|
+
function basename(filePath: string): string {
|
|
17
|
+
const idx = filePath.lastIndexOf("/");
|
|
18
|
+
return idx === -1 ? filePath : filePath.slice(idx + 1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Safely read a string field from tool_input. */
|
|
22
|
+
function inputStr(event: HookEvent, key: string): string | undefined {
|
|
23
|
+
const val = event.tool_input?.[key];
|
|
24
|
+
return typeof val === "string" ? val : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Safely read tool_response as a string. */
|
|
28
|
+
function responseStr(event: HookEvent): string {
|
|
29
|
+
if (typeof event.tool_response === "string") return event.tool_response;
|
|
30
|
+
if (event.tool_response != null) return String(event.tool_response);
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Test failure parser
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** A structured test failure parsed from test runner output. */
|
|
39
|
+
export interface TestFailure {
|
|
40
|
+
testName: string;
|
|
41
|
+
testFile: string;
|
|
42
|
+
errorMessage: string;
|
|
43
|
+
sourceFile?: string;
|
|
44
|
+
sourceLine?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const TEST_COMMAND_RE = /\b(test|vitest|jest|pytest|mocha)\b/;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse structured test failures from test runner output.
|
|
51
|
+
* Supports vitest/jest and pytest output formats.
|
|
52
|
+
*/
|
|
53
|
+
export function parseTestFailures(output: string, command: string): TestFailure[] {
|
|
54
|
+
if (!TEST_COMMAND_RE.test(command)) return [];
|
|
55
|
+
|
|
56
|
+
const failures: TestFailure[] = [];
|
|
57
|
+
|
|
58
|
+
// --- Vitest/Jest format ---
|
|
59
|
+
// Matches: ❯ path/to/test.ts (N tests | M failed)
|
|
60
|
+
// Then: × test name
|
|
61
|
+
// → ErrorType: message
|
|
62
|
+
// ❯ source.ts:line:col
|
|
63
|
+
const vitestBlockRe = /❯\s+(\S+\.(?:test|spec)\.\S+)\s+\([^)]*failed[^)]*\)/g;
|
|
64
|
+
let blockMatch: RegExpExecArray | null = vitestBlockRe.exec(output);
|
|
65
|
+
while (blockMatch !== null) {
|
|
66
|
+
const testFile = blockMatch[1];
|
|
67
|
+
// Find the region of this test file block
|
|
68
|
+
const blockStart = blockMatch.index + blockMatch[0].length;
|
|
69
|
+
const nextBlock = output.indexOf("\n ❯ ", blockStart);
|
|
70
|
+
const blockEnd = nextBlock === -1 ? output.length : nextBlock;
|
|
71
|
+
const blockText = output.slice(blockStart, blockEnd);
|
|
72
|
+
|
|
73
|
+
// Find individual failed tests: × test name
|
|
74
|
+
const failRe = /×\s+(.+)/g;
|
|
75
|
+
let failMatch: RegExpExecArray | null = failRe.exec(blockText);
|
|
76
|
+
while (failMatch !== null) {
|
|
77
|
+
const testName = failMatch[1].trim();
|
|
78
|
+
// Look for error message after the test name (→ ErrorType: msg)
|
|
79
|
+
const afterFail = blockText.slice(failMatch.index + failMatch[0].length);
|
|
80
|
+
const errorLineMatch = afterFail.match(/→\s+(.+)/);
|
|
81
|
+
const errorMessage = errorLineMatch ? errorLineMatch[1].trim() : "Test failed";
|
|
82
|
+
|
|
83
|
+
// Look for source file in stack trace (❯ path:line:col, skip test files)
|
|
84
|
+
const stackLines = afterFail.match(/❯\s+(\S+):(\d+):\d+/g) ?? [];
|
|
85
|
+
let sourceFile: string | undefined;
|
|
86
|
+
let sourceLine: number | undefined;
|
|
87
|
+
for (const sl of stackLines) {
|
|
88
|
+
const m = sl.match(/❯\s+(\S+):(\d+):\d+/);
|
|
89
|
+
if (m && !m[1].match(/\.(test|spec)\./)) {
|
|
90
|
+
sourceFile = m[1];
|
|
91
|
+
sourceLine = Number.parseInt(m[2], 10);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
failures.push({ testName, testFile, errorMessage, sourceFile, sourceLine });
|
|
97
|
+
failMatch = failRe.exec(blockText);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
blockMatch = vitestBlockRe.exec(output);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- Pytest format ---
|
|
104
|
+
// FAILED path::test_name - ErrorType: message
|
|
105
|
+
const pytestRe = /^FAILED\s+(\S+)::(\S+)\s+-\s+(.+)$/gm;
|
|
106
|
+
let pytestMatch: RegExpExecArray | null = pytestRe.exec(output);
|
|
107
|
+
while (pytestMatch !== null) {
|
|
108
|
+
failures.push({
|
|
109
|
+
testName: pytestMatch[2],
|
|
110
|
+
testFile: pytestMatch[1],
|
|
111
|
+
errorMessage: pytestMatch[3].trim(),
|
|
112
|
+
});
|
|
113
|
+
pytestMatch = pytestRe.exec(output);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return failures;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Tool-specific handlers
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
async function handleWrite(db: SiaDb, event: HookEvent): Promise<HookResponse> {
|
|
124
|
+
const filePath = inputStr(event, "file_path");
|
|
125
|
+
const content = inputStr(event, "content");
|
|
126
|
+
|
|
127
|
+
if (!filePath || !content) {
|
|
128
|
+
return { status: "skipped", nodes_created: 0 };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let nodesCreated = 0;
|
|
132
|
+
|
|
133
|
+
// 1. Insert a FileNode entity for the written file
|
|
134
|
+
await insertEntity(db, {
|
|
135
|
+
type: "FileNode",
|
|
136
|
+
name: basename(filePath),
|
|
137
|
+
content: content.slice(0, 2000),
|
|
138
|
+
summary: `File written: ${filePath}`,
|
|
139
|
+
file_paths: JSON.stringify([filePath]),
|
|
140
|
+
extraction_method: "hook:post-tool-use:write",
|
|
141
|
+
source_episode: event.session_id,
|
|
142
|
+
});
|
|
143
|
+
nodesCreated++;
|
|
144
|
+
|
|
145
|
+
// 2. Extract structural code entities via TrackA
|
|
146
|
+
const trackAFacts = extractTrackA(content, filePath);
|
|
147
|
+
for (const fact of trackAFacts) {
|
|
148
|
+
await insertEntity(db, {
|
|
149
|
+
type: fact.type,
|
|
150
|
+
name: fact.name,
|
|
151
|
+
content: fact.content,
|
|
152
|
+
summary: fact.summary,
|
|
153
|
+
tags: JSON.stringify(fact.tags),
|
|
154
|
+
file_paths: JSON.stringify(fact.file_paths),
|
|
155
|
+
trust_tier: fact.trust_tier,
|
|
156
|
+
confidence: fact.confidence,
|
|
157
|
+
extraction_method: "hook:post-tool-use:track-a",
|
|
158
|
+
source_episode: event.session_id,
|
|
159
|
+
});
|
|
160
|
+
nodesCreated++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 3. Detect knowledge patterns in the content
|
|
164
|
+
const patterns = detectKnowledgePatterns(content);
|
|
165
|
+
for (const p of patterns) {
|
|
166
|
+
await insertEntity(db, {
|
|
167
|
+
type: p.type,
|
|
168
|
+
name: `${p.type}: ${p.content.slice(0, 60)}`,
|
|
169
|
+
content: p.content,
|
|
170
|
+
summary: `${p.type} detected in ${basename(filePath)}`,
|
|
171
|
+
confidence: p.confidence,
|
|
172
|
+
file_paths: JSON.stringify([filePath]),
|
|
173
|
+
extraction_method: "hook:post-tool-use:pattern",
|
|
174
|
+
source_episode: event.session_id,
|
|
175
|
+
});
|
|
176
|
+
nodesCreated++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { status: "processed", nodes_created: nodesCreated };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function handleEdit(db: SiaDb, event: HookEvent): Promise<HookResponse> {
|
|
183
|
+
const filePath = inputStr(event, "file_path");
|
|
184
|
+
const oldStr = inputStr(event, "old_string");
|
|
185
|
+
const newStr = inputStr(event, "new_string");
|
|
186
|
+
|
|
187
|
+
if (!filePath) {
|
|
188
|
+
return { status: "skipped", nodes_created: 0 };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let nodesCreated = 0;
|
|
192
|
+
|
|
193
|
+
const editContent = oldStr && newStr ? `--- ${oldStr}\n+++ ${newStr}` : "edit";
|
|
194
|
+
|
|
195
|
+
// 1. Always create the EditEvent entity (existing behavior)
|
|
196
|
+
await insertEntity(db, {
|
|
197
|
+
type: "CodeEntity",
|
|
198
|
+
name: `Edit: ${basename(filePath)}`,
|
|
199
|
+
content: editContent.slice(0, 2000),
|
|
200
|
+
summary: `Edit in ${filePath}`,
|
|
201
|
+
file_paths: JSON.stringify([filePath]),
|
|
202
|
+
extraction_method: "hook:post-tool-use:edit",
|
|
203
|
+
source_episode: event.session_id,
|
|
204
|
+
kind: "EditEvent",
|
|
205
|
+
});
|
|
206
|
+
nodesCreated++;
|
|
207
|
+
|
|
208
|
+
// 2. Extract structural code entities from new_string via TrackA (best-effort)
|
|
209
|
+
if (newStr) {
|
|
210
|
+
try {
|
|
211
|
+
const trackAFacts = extractTrackA(newStr, filePath);
|
|
212
|
+
for (const fact of trackAFacts) {
|
|
213
|
+
await insertEntity(db, {
|
|
214
|
+
type: fact.type,
|
|
215
|
+
name: fact.name,
|
|
216
|
+
content: fact.content,
|
|
217
|
+
summary: fact.summary,
|
|
218
|
+
tags: JSON.stringify(fact.tags),
|
|
219
|
+
file_paths: JSON.stringify(fact.file_paths),
|
|
220
|
+
trust_tier: fact.trust_tier,
|
|
221
|
+
confidence: fact.confidence,
|
|
222
|
+
extraction_method: "hook:post-tool-use:track-a",
|
|
223
|
+
source_episode: event.session_id,
|
|
224
|
+
});
|
|
225
|
+
nodesCreated++;
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// Best-effort — don't block EditEvent creation
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 3. Detect knowledge patterns in new_string (best-effort)
|
|
232
|
+
try {
|
|
233
|
+
const patterns = detectKnowledgePatterns(newStr);
|
|
234
|
+
for (const p of patterns) {
|
|
235
|
+
await insertEntity(db, {
|
|
236
|
+
type: p.type,
|
|
237
|
+
name: `${p.type}: ${p.content.slice(0, 60)}`,
|
|
238
|
+
content: p.content,
|
|
239
|
+
summary: `${p.type} detected in ${basename(filePath)}`,
|
|
240
|
+
confidence: p.confidence,
|
|
241
|
+
file_paths: JSON.stringify([filePath]),
|
|
242
|
+
extraction_method: "hook:post-tool-use:pattern",
|
|
243
|
+
source_episode: event.session_id,
|
|
244
|
+
});
|
|
245
|
+
nodesCreated++;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// Best-effort — don't block EditEvent creation
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { status: "processed", nodes_created: nodesCreated };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function handleBash(db: SiaDb, event: HookEvent): Promise<HookResponse> {
|
|
256
|
+
const command = inputStr(event, "command") ?? "";
|
|
257
|
+
const output = responseStr(event);
|
|
258
|
+
|
|
259
|
+
let nodesCreated = 0;
|
|
260
|
+
|
|
261
|
+
// Always create an ExecutionEvent for the command
|
|
262
|
+
await insertEntity(db, {
|
|
263
|
+
type: "CodeEntity",
|
|
264
|
+
name: `Bash: ${command.slice(0, 80)}`,
|
|
265
|
+
content: `$ ${command}\n${output.slice(0, 2000)}`,
|
|
266
|
+
summary: `Executed: ${command.slice(0, 120)}`,
|
|
267
|
+
extraction_method: "hook:post-tool-use:bash",
|
|
268
|
+
source_episode: event.session_id,
|
|
269
|
+
kind: "ExecutionEvent",
|
|
270
|
+
});
|
|
271
|
+
nodesCreated++;
|
|
272
|
+
|
|
273
|
+
// Detect git commit messages
|
|
274
|
+
const commitMatch = command.match(/git\s+commit\s+.*-m\s+["'](.+?)["']/);
|
|
275
|
+
if (commitMatch) {
|
|
276
|
+
const commitMsg = commitMatch[1];
|
|
277
|
+
const commitPatterns = detectCommitPatterns(commitMsg);
|
|
278
|
+
for (const p of commitPatterns) {
|
|
279
|
+
await insertEntity(db, {
|
|
280
|
+
type: p.type,
|
|
281
|
+
name: `${p.type}: ${commitMsg.slice(0, 60)}`,
|
|
282
|
+
content: commitMsg,
|
|
283
|
+
summary: `Commit: ${commitMsg.slice(0, 120)}`,
|
|
284
|
+
confidence: p.confidence,
|
|
285
|
+
extraction_method: "hook:post-tool-use:commit",
|
|
286
|
+
source_episode: event.session_id,
|
|
287
|
+
});
|
|
288
|
+
nodesCreated++;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Detect structured test failures from test runner output
|
|
293
|
+
const testFailures = parseTestFailures(output, command);
|
|
294
|
+
for (const failure of testFailures) {
|
|
295
|
+
const filePaths = [failure.testFile];
|
|
296
|
+
if (failure.sourceFile) filePaths.push(failure.sourceFile);
|
|
297
|
+
|
|
298
|
+
const content = [
|
|
299
|
+
`Test: ${failure.testName}`,
|
|
300
|
+
`File: ${failure.testFile}`,
|
|
301
|
+
`Error: ${failure.errorMessage}`,
|
|
302
|
+
failure.sourceFile ? `Source: ${failure.sourceFile}:${failure.sourceLine ?? "?"}` : null,
|
|
303
|
+
]
|
|
304
|
+
.filter(Boolean)
|
|
305
|
+
.join("\n");
|
|
306
|
+
|
|
307
|
+
await insertEntity(db, {
|
|
308
|
+
type: "Bug",
|
|
309
|
+
name: `${failure.testName}: ${failure.errorMessage.slice(0, 60)}`,
|
|
310
|
+
content,
|
|
311
|
+
summary: `Test failure in ${failure.testFile}: ${failure.testName}`,
|
|
312
|
+
trust_tier: 2,
|
|
313
|
+
confidence: 0.95,
|
|
314
|
+
file_paths: JSON.stringify(filePaths),
|
|
315
|
+
extraction_method: "hook:post-tool-use:test-runner",
|
|
316
|
+
source_episode: event.session_id,
|
|
317
|
+
kind: "Bug",
|
|
318
|
+
});
|
|
319
|
+
nodesCreated++;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Detect errors in output (skip if structured test failures were found)
|
|
323
|
+
if (testFailures.length === 0) {
|
|
324
|
+
const hasError =
|
|
325
|
+
/\bError\b/i.test(output) ||
|
|
326
|
+
/\bfailed\b/i.test(output) ||
|
|
327
|
+
/\bFATAL\b/.test(output) ||
|
|
328
|
+
/\bpanic\b/i.test(output);
|
|
329
|
+
|
|
330
|
+
if (hasError && !commitMatch) {
|
|
331
|
+
await insertEntity(db, {
|
|
332
|
+
type: "Bug",
|
|
333
|
+
name: `Error: ${command.slice(0, 60)}`,
|
|
334
|
+
content: output.slice(0, 2000),
|
|
335
|
+
summary: `Error detected running: ${command.slice(0, 100)}`,
|
|
336
|
+
confidence: 0.7,
|
|
337
|
+
extraction_method: "hook:post-tool-use:error",
|
|
338
|
+
source_episode: event.session_id,
|
|
339
|
+
});
|
|
340
|
+
nodesCreated++;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { status: "processed", nodes_created: nodesCreated };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function handleRead(db: SiaDb, event: HookEvent): Promise<HookResponse> {
|
|
348
|
+
const filePath = inputStr(event, "file_path");
|
|
349
|
+
if (!filePath) {
|
|
350
|
+
return { status: "processed", nodes_created: 0, context: [] };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const result = await db.execute(
|
|
354
|
+
`SELECT id, type, name, summary, kind, trust_tier, confidence
|
|
355
|
+
FROM graph_nodes
|
|
356
|
+
WHERE file_paths LIKE ?
|
|
357
|
+
AND t_valid_until IS NULL
|
|
358
|
+
AND archived_at IS NULL
|
|
359
|
+
ORDER BY importance DESC
|
|
360
|
+
LIMIT 5`,
|
|
361
|
+
[`%${filePath}%`],
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const context = (result.rows as Array<Record<string, unknown>>).map((row) => ({
|
|
365
|
+
id: row.id,
|
|
366
|
+
type: row.type,
|
|
367
|
+
name: row.name,
|
|
368
|
+
summary: row.summary,
|
|
369
|
+
kind: row.kind,
|
|
370
|
+
trust_tier: row.trust_tier,
|
|
371
|
+
confidence: row.confidence,
|
|
372
|
+
}));
|
|
373
|
+
|
|
374
|
+
return { status: "processed", nodes_created: 0, context };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Factory
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Create a PostToolUse hook handler bound to the given graph database.
|
|
383
|
+
*/
|
|
384
|
+
export function createPostToolUseHandler(db: SiaDb): HookHandler {
|
|
385
|
+
return async (event: HookEvent): Promise<HookResponse> => {
|
|
386
|
+
if (!event.tool_name || !event.tool_input) {
|
|
387
|
+
return { status: "skipped", nodes_created: 0 };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
switch (event.tool_name) {
|
|
391
|
+
case "Write":
|
|
392
|
+
return handleWrite(db, event);
|
|
393
|
+
case "Edit":
|
|
394
|
+
return handleEdit(db, event);
|
|
395
|
+
case "Bash":
|
|
396
|
+
return handleBash(db, event);
|
|
397
|
+
case "Read":
|
|
398
|
+
return handleRead(db, event);
|
|
399
|
+
default:
|
|
400
|
+
return { status: "skipped", nodes_created: 0 };
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Module: pre-compact — PreCompact hook handler
|
|
2
|
+
//
|
|
3
|
+
// Fires before Claude Code compacts the context window. Reads the transcript
|
|
4
|
+
// for any remaining unextracted knowledge (using pattern detection), then
|
|
5
|
+
// creates a session snapshot in memory before the compaction discards detail.
|
|
6
|
+
//
|
|
7
|
+
// Returns { status: "processed", snapshot_nodes: N } where N is the count
|
|
8
|
+
// of patterns captured from the pre-compaction transcript scan.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
12
|
+
import { insertEntity } from "@/graph/entities";
|
|
13
|
+
import { detectKnowledgePatterns } from "@/hooks/extractors/pattern-detector";
|
|
14
|
+
import type { HookEvent, HookResponse } from "@/hooks/types";
|
|
15
|
+
|
|
16
|
+
/** How many lines from the tail of the transcript to scan before compaction. */
|
|
17
|
+
const COMPACT_SEGMENT_SIZE = 100;
|
|
18
|
+
|
|
19
|
+
/** Shape of a single JSONL transcript line. */
|
|
20
|
+
interface TranscriptLine {
|
|
21
|
+
role?: string;
|
|
22
|
+
content?: string;
|
|
23
|
+
tool_calls?: Array<{ name?: string; input?: Record<string, unknown> }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read and parse recent lines from a JSONL transcript file.
|
|
28
|
+
*/
|
|
29
|
+
function readTranscriptLines(path: string): TranscriptLine[] {
|
|
30
|
+
let raw: string;
|
|
31
|
+
try {
|
|
32
|
+
raw = readFileSync(path, "utf-8");
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
38
|
+
const recentLines = allLines.slice(-COMPACT_SEGMENT_SIZE);
|
|
39
|
+
|
|
40
|
+
const parsed: TranscriptLine[] = [];
|
|
41
|
+
for (const line of recentLines) {
|
|
42
|
+
try {
|
|
43
|
+
parsed.push(JSON.parse(line) as TranscriptLine);
|
|
44
|
+
} catch {
|
|
45
|
+
// Skip malformed lines
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Collect assistant message content strings from transcript lines.
|
|
54
|
+
*/
|
|
55
|
+
function collectAssistantContent(lines: TranscriptLine[]): string[] {
|
|
56
|
+
const contents: string[] = [];
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line.role === "assistant" && typeof line.content === "string") {
|
|
59
|
+
contents.push(line.content);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return contents;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a PreCompact hook handler bound to the given graph database.
|
|
67
|
+
*
|
|
68
|
+
* Scans the transcript tail for uncaptured knowledge patterns before
|
|
69
|
+
* the context window is compacted, ensuring no knowledge is lost.
|
|
70
|
+
*/
|
|
71
|
+
export function createPreCompactHandler(db: SiaDb): (event: HookEvent) => Promise<HookResponse> {
|
|
72
|
+
return async (event: HookEvent): Promise<HookResponse> => {
|
|
73
|
+
const lines = readTranscriptLines(event.transcript_path);
|
|
74
|
+
|
|
75
|
+
if (lines.length === 0) {
|
|
76
|
+
return { status: "processed", snapshot_nodes: 0 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const assistantContents = collectAssistantContent(lines);
|
|
80
|
+
let snapshotNodes = 0;
|
|
81
|
+
|
|
82
|
+
for (const content of assistantContents) {
|
|
83
|
+
const patterns = detectKnowledgePatterns(content);
|
|
84
|
+
for (const p of patterns) {
|
|
85
|
+
await insertEntity(db, {
|
|
86
|
+
type: p.type,
|
|
87
|
+
name: `${p.type}: ${p.content.slice(0, 60)}`,
|
|
88
|
+
content: p.content,
|
|
89
|
+
summary: `${p.type} captured pre-compaction in session ${event.session_id}`,
|
|
90
|
+
confidence: p.confidence,
|
|
91
|
+
extraction_method: "hook:pre-compact:pattern",
|
|
92
|
+
source_episode: event.session_id,
|
|
93
|
+
});
|
|
94
|
+
snapshotNodes++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { status: "processed", snapshot_nodes: snapshotNodes };
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Module: session-end — SessionEnd hook handler
|
|
2
|
+
//
|
|
3
|
+
// Fires when a Claude Code session ends (exit, sigint, or error).
|
|
4
|
+
// Records session statistics and can update the session entity's
|
|
5
|
+
// ended_at timestamp in the graph.
|
|
6
|
+
//
|
|
7
|
+
// Returns { status: "processed", nodes_this_session: N } where N is
|
|
8
|
+
// the count of entities created during this session.
|
|
9
|
+
|
|
10
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
11
|
+
import type { HookEvent, HookResponse } from "@/hooks/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Count the number of entities created during a given session.
|
|
15
|
+
* Looks up all entities whose source_episode matches the session_id.
|
|
16
|
+
*/
|
|
17
|
+
async function countSessionEntities(db: SiaDb, sessionId: string): Promise<number> {
|
|
18
|
+
const result = await db.execute(
|
|
19
|
+
"SELECT COUNT(*) as count FROM graph_nodes WHERE source_episode = ?",
|
|
20
|
+
[sessionId],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const row = result.rows[0];
|
|
24
|
+
if (!row) return 0;
|
|
25
|
+
|
|
26
|
+
const count = row.count as number | bigint;
|
|
27
|
+
return typeof count === "bigint" ? Number(count) : (count ?? 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a SessionEnd hook handler bound to the given graph database.
|
|
32
|
+
*
|
|
33
|
+
* On session end:
|
|
34
|
+
* 1. Counts all entities created during this session (via source_episode).
|
|
35
|
+
* 2. Returns session statistics for observability.
|
|
36
|
+
*/
|
|
37
|
+
export function createSessionEndHandler(db: SiaDb): (event: HookEvent) => Promise<HookResponse> {
|
|
38
|
+
return async (event: HookEvent): Promise<HookResponse> => {
|
|
39
|
+
const sessionId = event.session_id;
|
|
40
|
+
const nodesThisSession = await countSessionEntities(db, sessionId);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
status: "processed",
|
|
44
|
+
nodes_this_session: nodesThisSession,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Module: session-start — SessionStart command hook handler
|
|
2
|
+
//
|
|
3
|
+
// Fires when Claude Code starts a new session (or resumes an existing one).
|
|
4
|
+
// This handler runs as a command hook — it writes context to stdout, which
|
|
5
|
+
// Claude Code injects into the initial system prompt.
|
|
6
|
+
//
|
|
7
|
+
// Queries the graph for:
|
|
8
|
+
// - Recent Decisions (limit 5) — architectural choices made in prior sessions
|
|
9
|
+
// - All active Conventions — coding patterns the team follows
|
|
10
|
+
// - Unresolved Bugs/ErrorEvents (limit 3) — known issues to watch out for
|
|
11
|
+
|
|
12
|
+
import type { SiaDb } from "@/graph/db-interface";
|
|
13
|
+
import type { HookEvent } from "@/hooks/types";
|
|
14
|
+
|
|
15
|
+
/** A single item summarised for session context injection. */
|
|
16
|
+
export interface ContextItem {
|
|
17
|
+
name: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The structured context gathered at session start. */
|
|
22
|
+
export interface SessionContext {
|
|
23
|
+
decisions: Array<ContextItem>;
|
|
24
|
+
conventions: Array<ContextItem>;
|
|
25
|
+
errors: Array<ContextItem>;
|
|
26
|
+
resuming: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Internal helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Query active entities of a given type from the graph database.
|
|
35
|
+
* Returns rows mapped to { name, summary } pairs.
|
|
36
|
+
*/
|
|
37
|
+
async function queryByType(db: SiaDb, type: string, limit?: number): Promise<Array<ContextItem>> {
|
|
38
|
+
const sql = limit
|
|
39
|
+
? "SELECT name, summary FROM graph_nodes WHERE type = ? AND t_valid_until IS NULL AND archived_at IS NULL ORDER BY created_at DESC LIMIT ?"
|
|
40
|
+
: "SELECT name, summary FROM graph_nodes WHERE type = ? AND t_valid_until IS NULL AND archived_at IS NULL ORDER BY created_at DESC";
|
|
41
|
+
|
|
42
|
+
const params: unknown[] = limit ? [type, limit] : [type];
|
|
43
|
+
const result = await db.execute(sql, params);
|
|
44
|
+
|
|
45
|
+
return result.rows.map((row) => ({
|
|
46
|
+
name: (row.name as string) ?? "",
|
|
47
|
+
summary: (row.summary as string) ?? "",
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Public API
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Query the graph for relevant context and format for injection.
|
|
57
|
+
*
|
|
58
|
+
* - Decisions: most recent 5 (architectural choices)
|
|
59
|
+
* - Conventions: all active (coding patterns)
|
|
60
|
+
* - Errors: most recent 3 unresolved Bugs / ErrorEvents
|
|
61
|
+
*/
|
|
62
|
+
export async function buildSessionContext(
|
|
63
|
+
db: SiaDb,
|
|
64
|
+
_cwd: string,
|
|
65
|
+
isResume: boolean,
|
|
66
|
+
): Promise<SessionContext> {
|
|
67
|
+
const [decisions, conventions, bugs, errorEvents] = await Promise.all([
|
|
68
|
+
queryByType(db, "Decision", 5),
|
|
69
|
+
queryByType(db, "Convention"),
|
|
70
|
+
queryByType(db, "Bug", 3),
|
|
71
|
+
queryByType(db, "ErrorEvent", 3),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// Merge bugs and error events, keeping only 3 total
|
|
75
|
+
const errors = [...bugs, ...errorEvents].slice(0, 3);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
decisions,
|
|
79
|
+
conventions,
|
|
80
|
+
errors,
|
|
81
|
+
resuming: isResume,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format the context as a concise markdown block for stdout output.
|
|
87
|
+
* Claude Code injects this as context at the top of the session.
|
|
88
|
+
*/
|
|
89
|
+
export function formatSessionContext(context: SessionContext): string {
|
|
90
|
+
const lines: string[] = [];
|
|
91
|
+
|
|
92
|
+
const header = context.resuming
|
|
93
|
+
? "## Sia — Resuming Session Context"
|
|
94
|
+
: "## Sia — Session Knowledge Context";
|
|
95
|
+
|
|
96
|
+
lines.push(header);
|
|
97
|
+
lines.push("");
|
|
98
|
+
|
|
99
|
+
if (context.decisions.length > 0) {
|
|
100
|
+
lines.push("### Recent Decisions");
|
|
101
|
+
for (const d of context.decisions) {
|
|
102
|
+
lines.push(`- **${d.name}**: ${d.summary}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push("");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (context.conventions.length > 0) {
|
|
108
|
+
lines.push("### Active Conventions");
|
|
109
|
+
for (const c of context.conventions) {
|
|
110
|
+
lines.push(`- **${c.name}**: ${c.summary}`);
|
|
111
|
+
}
|
|
112
|
+
lines.push("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (context.errors.length > 0) {
|
|
116
|
+
lines.push("### Known Issues");
|
|
117
|
+
for (const e of context.errors) {
|
|
118
|
+
lines.push(`- **${e.name}**: ${e.summary}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
context.decisions.length === 0 &&
|
|
125
|
+
context.conventions.length === 0 &&
|
|
126
|
+
context.errors.length === 0
|
|
127
|
+
) {
|
|
128
|
+
lines.push("_No prior session knowledge found for this repository._");
|
|
129
|
+
lines.push("");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Lightweight nudge — CLAUDE.md has the full behavioral spec
|
|
133
|
+
lines.push("---");
|
|
134
|
+
lines.push("Sia memory tools are active. See CLAUDE.md for tool selection guidance.");
|
|
135
|
+
lines.push("");
|
|
136
|
+
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// CLI entry point
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Main entry point for the `sia hook session-start` command.
|
|
146
|
+
* Reads hook event JSON from stdin, builds context, and writes to stdout.
|
|
147
|
+
* Claude Code reads the stdout output and injects it into the session.
|
|
148
|
+
*/
|
|
149
|
+
export async function runSessionStartHook(db: SiaDb, event: HookEvent): Promise<void> {
|
|
150
|
+
const isResume = event.source === "resume";
|
|
151
|
+
const context = await buildSessionContext(db, event.cwd, isResume);
|
|
152
|
+
const formatted = formatSessionContext(context);
|
|
153
|
+
process.stdout.write(formatted);
|
|
154
|
+
}
|