septum 0.1.6 → 0.1.8
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/README.md +38 -37
- package/package.json +6 -5
- package/src/cli/commands/check.ts +34 -122
- package/src/cli/commands/hook.ts +68 -16
- package/src/cli/commands/init.ts +440 -121
- package/src/cli/commands/sync.ts +27 -17
- package/src/cli/commands/topology.ts +89 -47
- package/src/cli/index.ts +7 -68
- package/src/cli/scaffold/harness-config.ts +697 -0
- package/src/cli/templates/hooks.ts +6 -6
- package/src/cli/templates/rules.ts +8 -8
- package/src/cli/templates/skill.ts +11 -18
- package/src/cli/ui/prompts.ts +122 -0
- package/src/core/boundary/evaluator.ts +21 -25
- package/src/core/boundary/staged-auditor.ts +50 -0
- package/src/core/config/loader.ts +21 -24
- package/src/core/database/client.ts +8 -0
- package/src/core/database/repositories/dependency.repository.ts +80 -16
- package/src/core/database/repositories/domain.repository.ts +86 -33
- package/src/core/database/repositories/symbol.repository.ts +147 -65
- package/src/core/database/repository.ts +17 -5
- package/src/core/database/schema.ts +1 -1
- package/src/core/discovery/topology-detector.ts +71 -19
- package/src/core/ingestion/pipeline.ts +274 -126
- package/src/core/parser/extractors/frontend.ts +1 -0
- package/src/core/parser/extractors/go.ts +2 -2
- package/src/core/parser/extractors/laravel-semantic.ts +9 -8
- package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
- package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
- package/src/core/parser/extractors/php.ts +2 -2
- package/src/core/parser/extractors/python.ts +2 -2
- package/src/core/parser/extractors/typescript.ts +2 -2
- package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
- package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
- package/src/core/parser/tree-sitter.ts +6 -1
- package/src/core/parser/wasm-assets.ts +17 -0
- package/src/core/parser/wasm-loader.ts +3 -0
- package/src/core/resolver/call-graph-tracer.ts +4 -3
- package/src/core/resolver/module-resolver.ts +1 -10
- package/src/core/resolver/path-resolver.ts +57 -4
- package/src/core/resolver/vertical-slice-tracer.ts +1 -0
- package/src/core/resources/ignore.ts +35 -0
- package/src/core/resources/limits.ts +14 -0
- package/src/core/resources/source-files.ts +62 -0
- package/src/core/resources/work-queue.ts +74 -0
- package/src/core/session/session-manager.ts +67 -38
- package/src/core/telemetry/telemetry.ts +12 -1
- package/src/core/topology/environment-detector.ts +21 -11
- package/src/mcp/schemas.ts +381 -8
- package/src/mcp/server.ts +326 -529
- package/src/mcp/tools/boundary.ts +17 -0
- package/src/mcp/tools/check-boundary.ts +97 -43
- package/src/mcp/tools/domain.ts +40 -0
- package/src/mcp/tools/feature.ts +67 -0
- package/src/mcp/tools/get-domain-catalog.ts +15 -7
- package/src/mcp/tools/get-environment-topology.ts +2 -2
- package/src/mcp/tools/get-feature-context.ts +9 -12
- package/src/mcp/tools/get-symbol-impact.ts +5 -2
- package/src/mcp/tools/get-symbol.ts +2 -1
- package/src/mcp/tools/locate-symbol.ts +28 -5
- package/src/mcp/tools/register-domain.ts +25 -9
- package/src/mcp/tools/register-feature.ts +40 -21
- package/src/mcp/tools/slice.ts +15 -0
- package/src/mcp/tools/symbol.ts +82 -0
- package/src/mcp/workspace-manager.ts +159 -0
- package/src/mcp/workspace-watcher.ts +202 -0
- package/src/types/index.ts +21 -1
- package/src/types/wasm.d.ts +4 -0
- package/src/cli/commands/ingest.ts +0 -29
- package/src/cli/commands/locate.ts +0 -61
- package/src/cli/commands/query.ts +0 -104
- package/src/cli/commands/slice.ts +0 -50
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> **Deterministic Bounded-Context & Anti-Poisoning Architecture Guard for AI Coding Agents.**
|
|
4
4
|
|
|
5
|
-
[](package.json)
|
|
6
6
|
[](https://bun.sh)
|
|
7
7
|
[](https://modelcontextprotocol.io/)
|
|
8
8
|
[-003B57?logo=sqlite)](https://sqlite.org/)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
|
|
11
|
-
Septum
|
|
11
|
+
Septum helps AI coding agents navigate code and respect declared architectural boundaries. It indexes your codebase into an embedded SQLite SSOT (`.septum/septum.db`), enforces strict bounded-context domains, and intercepts unauthorized cross-domain edits before they hit disk.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -28,16 +28,26 @@ bun add -g septum
|
|
|
28
28
|
|
|
29
29
|
### 1. Initialize in Any Project
|
|
30
30
|
|
|
31
|
+
Run `septum init` in your project root:
|
|
32
|
+
|
|
31
33
|
```bash
|
|
32
34
|
septum init
|
|
33
35
|
# or zero-install via bunx:
|
|
34
36
|
bunx septum init
|
|
35
37
|
```
|
|
36
|
-
*Auto-detects project topology (Monorepos, Laravel, Next.js, Go, NestJS), registers domains, and ingests AST symbols into `.septum/septum.db`.*
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
Septum automatically:
|
|
40
|
+
1. **Detects Architecture & Topology:** Discovers bounded domains across Monorepos, Laravel, Next.js, Go, NestJS, and more.
|
|
41
|
+
2. **Builds AST Symbol SSOT:** Ingests symbols, interfaces, and dependencies into `.septum/septum.db`.
|
|
42
|
+
3. **Scaffolds AI Agent Harnesses:** Interactively configures MCP servers, guardrail rules, and pre-write boundary hooks for your favorite agent environments:
|
|
43
|
+
- **Claude Code** (`.mcp.json`, `.claude/settings.json`)
|
|
44
|
+
- **Antigravity** (`.gemini/`)
|
|
45
|
+
- **Cursor** (`.cursor/mcp.json`, `.cursor/hooks.json`)
|
|
46
|
+
- **Opencode** (`opencode.json`)
|
|
47
|
+
|
|
48
|
+
### 2. Manual MCP Client Configuration (Optional)
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
If your MCP client is not configured via `septum init`, add Septum manually (`mcp.json` or `claude_desktop_config.json`):
|
|
41
51
|
|
|
42
52
|
```json
|
|
43
53
|
{
|
|
@@ -57,15 +67,12 @@ Add Septum to your MCP client (`mcp.json` or `claude_desktop_config.json`):
|
|
|
57
67
|
|
|
58
68
|
| Command | Description |
|
|
59
69
|
|---|---|
|
|
60
|
-
| `septum init` | Detect topology, scaffold boundary guards, and build initial catalog |
|
|
61
|
-
| `septum
|
|
62
|
-
| `septum
|
|
63
|
-
| `septum
|
|
64
|
-
| `septum
|
|
65
|
-
| `septum
|
|
66
|
-
| `septum slice <route\|intent>` | Trace end-to-end vertical execution slice for route/intent |
|
|
67
|
-
| `septum topology [action]` | Inspect 6-layer environment topology plane & distributed nodes |
|
|
68
|
-
| `septum feature status\|clear` | Manage active feature lease and editing scope |
|
|
70
|
+
| `septum init [--force]` | Detect topology, scaffold boundary guards, and build initial catalog. `--force` regenerates Septum-owned hooks and harness artifacts while strictly preserving SQLite DB, manual domain policies, and user Git hooks |
|
|
71
|
+
| `septum sync [--json]` | AST synchronization and catalog update into `.septum/septum.db` (full or incremental) |
|
|
72
|
+
| `septum check [--strict] [--staged] [--feature <name>] [--json]` | CI/CD gate & pre-commit hook: verify boundary integrity & fail on cross-domain violations |
|
|
73
|
+
| `septum feature [status|clear] [--force]` | View or release active feature lease and editing scope (`--force` for other owner's session) |
|
|
74
|
+
| `septum hook [subhook]` | Internal pre-tool guardrail hook (e.g. pre-write, post-write) for AI agent tools |
|
|
75
|
+
| `septum topology [subcommand]` | Inspect 6-layer environment topology plane & distributed nodes (`env` alias available). Subcommands: `resolve <layer> <platform>`, `add-node`, `remove-node`, `clear` |
|
|
69
76
|
| `septum serve` | Launch stdio MCP server for AI coding agents |
|
|
70
77
|
|
|
71
78
|
---
|
|
@@ -74,32 +81,26 @@ Add Septum to your MCP client (`mcp.json` or `claude_desktop_config.json`):
|
|
|
74
81
|
|
|
75
82
|
All tools expose typed schemas automatically via the Model Context Protocol:
|
|
76
83
|
|
|
77
|
-
- **
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
- **
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
cd septum
|
|
94
|
-
bun install
|
|
95
|
-
|
|
96
|
-
# Run test suite & build
|
|
97
|
-
bun test
|
|
98
|
-
bun run build
|
|
99
|
-
```
|
|
84
|
+
- **Domain Catalog, Registration & Environment Topology (`septum_domain`):**
|
|
85
|
+
- `action: "catalog"`: Whole-project macro architecture map and file connections in < 400 tokens (`detail_level: "compact" | "full"`).
|
|
86
|
+
- `action: "register"`: Dynamically registers or updates bounded-context domains with allowed/forbidden dependencies and layer archetypes.
|
|
87
|
+
- `action: "topology"`: Maps and verifies the 6-layer production architecture (Edge/WAF, Gateway/Web Server, Host, Runtime, Storage/Cache, Telemetry) and distributed nodes with cross-layer invariants.
|
|
88
|
+
- **Unified Symbol Engine (`septum_symbol`):**
|
|
89
|
+
- `action: "locate"`: Fast fuzzy search and error log resolution to pinpoint symbols across workspaces.
|
|
90
|
+
- `action: "inspect"`: Deep inspection of physical coordinates, declarations, and full typed signatures.
|
|
91
|
+
- `action: "impact"`: Refactoring blast radius, call graphs, and inbound/outbound dependency analysis.
|
|
92
|
+
- `action: "hotspots"`: God function and Single Responsibility Principle (SRP) detector ranked by physical lines, cyclomatic nesting depth, and composite risk score.
|
|
93
|
+
- **Omni-Trigger Vertical Slice Tracer (`septum_slice`):**
|
|
94
|
+
- Traces end-to-end execution flow (*Ingress ➔ Validation ➔ Controller ➔ Service ➔ Repository ➔ Entity*) from route URIs, controller actions, or high-level intent across HTTP routes, queue jobs, console commands, and events up to 8 levels deep.
|
|
95
|
+
- **Architecture Guard & Circular Dependency Gate (`septum_boundary`):**
|
|
96
|
+
- Validates proposed edits, additions, and imports against domain rules and feature touchpoints before files are modified on disk (`scope: "file"`).
|
|
97
|
+
- Audits staged files (`scope: "staged"`), domain boundaries (`scope: "domain"`), or the entire repository (`scope: "all"`) for cross-domain leaks and dependency cycles.
|
|
98
|
+
- **Active Touchpoint & Scope Lock Engine (`septum_feature`):**
|
|
99
|
+
- Locks active feature task context, touchpoints, and contracts (`action: "lock"` / `action: "get"` / `action: "release"`) via `.septum/session.json` to eliminate AI context drift and unintended edits.
|
|
100
100
|
|
|
101
101
|
---
|
|
102
102
|
|
|
103
103
|
## License
|
|
104
104
|
|
|
105
105
|
[MIT](LICENSE) © shironim
|
|
106
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "septum",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Deterministic Bounded-Context, Structural File Catalog & Anti-Poisoning Partition for AI Agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -18,13 +18,16 @@
|
|
|
18
18
|
"check": "bun run bin/septum.ts check",
|
|
19
19
|
"test": "bun test",
|
|
20
20
|
"build": "bun build ./bin/septum.ts --compile --outfile dist/septum",
|
|
21
|
-
"typecheck": "tsc --noEmit"
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test:integration": "python3 tests/integration/mcp-reliability.py",
|
|
23
|
+
"postinstall": "bun run scripts/postinstall.ts || true"
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
24
26
|
"@modelcontextprotocol/sdk": "^1.5.0",
|
|
25
27
|
"web-tree-sitter": "^0.24.0",
|
|
26
28
|
"yaml": "^2.7.0",
|
|
27
|
-
"zod": "^3.24.2"
|
|
29
|
+
"zod": "^3.24.2",
|
|
30
|
+
"tree-sitter-wasms": "^0.1.13"
|
|
28
31
|
},
|
|
29
32
|
"license": "MIT",
|
|
30
33
|
"repository": {
|
|
@@ -56,8 +59,6 @@
|
|
|
56
59
|
"devDependencies": {
|
|
57
60
|
"@types/bun": "latest",
|
|
58
61
|
"@types/node": "^22.13.4",
|
|
59
|
-
"tree-sitter-wasms": "^0.1.13",
|
|
60
62
|
"typescript": "^5.7.3"
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
|
-
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
1
|
import { BoundaryEvaluator } from "../../core/boundary/evaluator.ts";
|
|
5
|
-
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
6
2
|
import { ConfigLoader } from "../../core/config/loader.ts";
|
|
7
3
|
import { SeptumDatabase } from "../../core/database/client.ts";
|
|
8
4
|
import { SeptumRepository } from "../../core/database/repository.ts";
|
|
9
|
-
import { ASTParserEngine } from "../../core/parser/tree-sitter.ts";
|
|
10
5
|
import type { BoundaryViolation } from "../../types/index.ts";
|
|
6
|
+
import { IngestionPipeline } from "../../core/ingestion/pipeline.ts";
|
|
7
|
+
import { auditStagedFiles } from "../../core/boundary/staged-auditor.ts";
|
|
11
8
|
|
|
12
9
|
export interface CheckCommandOptions {
|
|
13
10
|
strict?: boolean;
|
|
@@ -28,134 +25,49 @@ export async function handleCheckCommand(
|
|
|
28
25
|
);
|
|
29
26
|
|
|
30
27
|
const config = ConfigLoader.load();
|
|
28
|
+
if (options.strict) config.settings.enforcement = "strict";
|
|
31
29
|
const db = new SeptumDatabase(config.settings.db_path);
|
|
32
30
|
const repo = new SeptumRepository(db.raw);
|
|
33
31
|
const evaluator = new BoundaryEvaluator(repo);
|
|
34
32
|
|
|
35
|
-
let violations: BoundaryViolation[] = [];
|
|
36
|
-
|
|
37
|
-
if (options.staged) {
|
|
38
|
-
violations = await auditStagedFiles(evaluator, config, options.feature);
|
|
39
|
-
} else {
|
|
40
|
-
violations = evaluator.auditCodebase(config);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (violations.length === 0) {
|
|
44
|
-
console.log("✓ All domain boundaries are clean. Zero cross-domain leaks detected.");
|
|
45
|
-
db.close();
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
console.error(`\n[Septum Boundary Violation] Found ${violations.length} boundary violation(s):\n`);
|
|
50
|
-
|
|
51
|
-
for (const v of violations) {
|
|
52
|
-
console.error(` ✕ [${v.rule.toUpperCase()}] at ${v.file}:${v.line}`);
|
|
53
|
-
console.error(` Source Domain: ${v.source_domain}`);
|
|
54
|
-
console.error(` Target Domain: ${v.target_domain}`);
|
|
55
|
-
console.error(` Import Target: ${v.imported_target}`);
|
|
56
|
-
console.error(` Message: ${v.message}\n`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
db.close();
|
|
60
|
-
|
|
61
|
-
const isStrict = options.strict || config.settings.enforcement === "strict";
|
|
62
|
-
if (isStrict) {
|
|
63
|
-
console.error("[Septum Failure] Process terminated with exit code 1 due to boundary violations.");
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function auditStagedFiles(
|
|
69
|
-
evaluator: BoundaryEvaluator,
|
|
70
|
-
config: ValidatedSeptumConfig,
|
|
71
|
-
featureKey?: string
|
|
72
|
-
): Promise<BoundaryViolation[]> {
|
|
73
|
-
const violations: BoundaryViolation[] = [];
|
|
74
|
-
let stagedFiles: string[] = [];
|
|
75
|
-
|
|
76
33
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (stagedFiles.length === 0) {
|
|
93
|
-
console.log(" Notice: No staged files found in git repository.");
|
|
94
|
-
return violations;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const astEngine = new ASTParserEngine();
|
|
98
|
-
|
|
99
|
-
for (const relPath of stagedFiles) {
|
|
100
|
-
const fullPath = path.resolve(process.cwd(), relPath);
|
|
101
|
-
if (!fs.existsSync(fullPath)) continue;
|
|
102
|
-
|
|
103
|
-
// 1. Feature touchpoint check if featureKey provided
|
|
104
|
-
if (featureKey) {
|
|
105
|
-
const tpViolations = evaluator.checkFeatureTouchpoints(featureKey, relPath, config);
|
|
106
|
-
violations.push(...tpViolations);
|
|
34
|
+
let violations: BoundaryViolation[] = [];
|
|
35
|
+
|
|
36
|
+
if (options.staged) {
|
|
37
|
+
violations = (await auditStagedFiles(evaluator, config, process.cwd(), options.feature, repo)).violations;
|
|
38
|
+
} else {
|
|
39
|
+
await new IngestionPipeline(repo).run(config, { verifyContent: true });
|
|
40
|
+
const unsupported = JSON.parse(repo.getMeta("unsupported_files") || "[]") as string[];
|
|
41
|
+
if (unsupported.length > 0) {
|
|
42
|
+
throw new Error(`Cannot complete boundary audit: unsupported source files: ${unsupported.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
const skipped = JSON.parse(repo.getMeta("skipped_files") || "[]") as {path: string}[];
|
|
45
|
+
if (skipped.length) throw new Error(`Cannot complete boundary audit: files exceed resource limits: ${skipped.map(file => file.path).join(", ")}`);
|
|
46
|
+
violations = evaluator.auditCodebase(config);
|
|
107
47
|
}
|
|
108
48
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!sourceConfig) continue;
|
|
114
|
-
|
|
115
|
-
const forbidden = sourceConfig.forbidden_dependencies ?? [];
|
|
116
|
-
const allowed = sourceConfig.allowed_dependencies ?? [];
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
120
|
-
const parsed = await astEngine.parseFile(fullPath, content);
|
|
49
|
+
if (violations.length === 0) {
|
|
50
|
+
console.log("✓ All domain boundaries are clean. Zero cross-domain leaks detected.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
121
53
|
|
|
122
|
-
|
|
123
|
-
if (dep.is_external) continue;
|
|
54
|
+
console.error(`\n[Septum Boundary Violation] Found ${violations.length} boundary violation(s):\n`);
|
|
124
55
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
56
|
+
for (const v of violations) {
|
|
57
|
+
console.error(` ✕ [${v.rule.toUpperCase()}] at ${v.file}:${v.line}`);
|
|
58
|
+
console.error(` Source Domain: ${v.source_domain}`);
|
|
59
|
+
console.error(` Target Domain: ${v.target_domain}`);
|
|
60
|
+
console.error(` Import Target: ${v.imported_target}`);
|
|
61
|
+
console.error(` Message: ${v.message}\n`);
|
|
62
|
+
}
|
|
130
63
|
|
|
131
|
-
if (!targetDomain || targetDomain === sourceDomain) continue;
|
|
132
64
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
source_domain: sourceDomain,
|
|
138
|
-
target_domain: targetDomain,
|
|
139
|
-
imported_target: dep.target,
|
|
140
|
-
rule: "forbidden_dependency",
|
|
141
|
-
message: `Illegal import: Domain '${sourceDomain}' is strictly prohibited from importing domain '${targetDomain}'.`,
|
|
142
|
-
});
|
|
143
|
-
} else if (config.settings.enforcement === "strict" && !allowed.includes(targetDomain)) {
|
|
144
|
-
violations.push({
|
|
145
|
-
file: relPath,
|
|
146
|
-
line: dep.line_number,
|
|
147
|
-
source_domain: sourceDomain,
|
|
148
|
-
target_domain: targetDomain,
|
|
149
|
-
imported_target: dep.target,
|
|
150
|
-
rule: "disallowed_dependency",
|
|
151
|
-
message: `Disallowed dependency: '${targetDomain}' is not declared in allowed_dependencies for domain '${sourceDomain}'.`,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
} catch {
|
|
156
|
-
// Ignore unparseable files
|
|
65
|
+
const isStrict = options.strict || config.settings.enforcement === "strict";
|
|
66
|
+
if (isStrict) {
|
|
67
|
+
console.error("[Septum Failure] Process terminated with exit code 1 due to boundary violations.");
|
|
68
|
+
process.exitCode = 1;
|
|
157
69
|
}
|
|
70
|
+
} finally {
|
|
71
|
+
db.close();
|
|
158
72
|
}
|
|
159
|
-
|
|
160
|
-
return violations;
|
|
161
73
|
}
|
package/src/cli/commands/hook.ts
CHANGED
|
@@ -7,8 +7,14 @@ import { SeptumRepository } from "../../core/database/repository.ts";
|
|
|
7
7
|
import { ASTParserEngine } from "../../core/parser/tree-sitter.ts";
|
|
8
8
|
import { SessionManager } from "../../core/session/session-manager.ts";
|
|
9
9
|
|
|
10
|
+
interface ToolCallInfo {
|
|
11
|
+
name?: string;
|
|
12
|
+
args?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export interface ToolPayload {
|
|
11
16
|
tool_name?: string;
|
|
17
|
+
toolCall?: ToolCallInfo;
|
|
12
18
|
tool_input?: {
|
|
13
19
|
TargetFile?: string;
|
|
14
20
|
file_path?: string;
|
|
@@ -30,8 +36,13 @@ export interface ToolPayload {
|
|
|
30
36
|
CodeContent?: string;
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
let configuredExitCode = 1;
|
|
40
|
+
|
|
33
41
|
export async function handleHookCommand(args: string[]): Promise<void> {
|
|
34
42
|
const subHook = args[1] || "pre-write";
|
|
43
|
+
if (args.includes("--exit-code-2") || args.includes("--claude") || args.includes("--cursor")) {
|
|
44
|
+
configuredExitCode = 2;
|
|
45
|
+
}
|
|
35
46
|
|
|
36
47
|
if (subHook === "pre-write") {
|
|
37
48
|
await runPreWriteHook();
|
|
@@ -40,6 +51,34 @@ export async function handleHookCommand(args: string[]): Promise<void> {
|
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
function blockHook(db: SeptumDatabase | null, reason: string): never {
|
|
55
|
+
if (db) {
|
|
56
|
+
try {
|
|
57
|
+
db.close();
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Print readable error to stderr for humans and Claude/Cursor/Codex
|
|
64
|
+
console.error(reason);
|
|
65
|
+
|
|
66
|
+
// Print JSON contract to stdout for Antigravity hook engine
|
|
67
|
+
try {
|
|
68
|
+
process.stdout.write(
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
decision: "deny",
|
|
71
|
+
reason: reason.trim(),
|
|
72
|
+
}) + "\n"
|
|
73
|
+
);
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Exit code: 1 by default (Unix standard), or 2 if specified for Claude Code / Cursor hooks
|
|
79
|
+
process.exit(configuredExitCode);
|
|
80
|
+
}
|
|
81
|
+
|
|
43
82
|
async function runPreWriteHook(): Promise<void> {
|
|
44
83
|
// If .septum/septum.db does not exist, pass through
|
|
45
84
|
const dbPath = path.resolve(process.cwd(), ".septum", "septum.db");
|
|
@@ -62,7 +101,11 @@ async function runPreWriteHook(): Promise<void> {
|
|
|
62
101
|
}
|
|
63
102
|
|
|
64
103
|
const toolInput: Record<string, any> =
|
|
65
|
-
payload.tool_input ||
|
|
104
|
+
payload.tool_input ||
|
|
105
|
+
payload.arguments ||
|
|
106
|
+
payload.toolCall?.args ||
|
|
107
|
+
(payload as Record<string, any>) ||
|
|
108
|
+
{};
|
|
66
109
|
const targetFile: string | undefined =
|
|
67
110
|
toolInput.TargetFile || toolInput.file_path || toolInput.filePath || toolInput.path;
|
|
68
111
|
const chunkContent = Array.isArray(toolInput.ReplacementChunks)
|
|
@@ -115,27 +158,28 @@ async function runPreWriteHook(): Promise<void> {
|
|
|
115
158
|
}
|
|
116
159
|
|
|
117
160
|
if (!hasConsulted) {
|
|
118
|
-
|
|
161
|
+
blockHook(
|
|
162
|
+
db,
|
|
119
163
|
`\n[SEPTUM GATE 3 VIOLATION - Mandatory Consultation Required]\n` +
|
|
120
164
|
`✕ Operation blocked: You are attempting to write to domain '${sourceDomain}' (${targetFile}) ` +
|
|
121
165
|
`without inspecting its authoritative symbol & file-connection catalog first.\n` +
|
|
122
|
-
`▶ REQUIRED ACTION: Call tool '
|
|
123
|
-
`or '
|
|
166
|
+
`▶ REQUIRED ACTION: Call tool 'septum_domain({ action: "catalog", domain: "${sourceDomain}", detail_level: "compact" })' ` +
|
|
167
|
+
`or 'septum_feature({ action: "get" })' before writing code.\n`
|
|
124
168
|
);
|
|
125
|
-
db.close();
|
|
126
|
-
process.exit(1);
|
|
127
169
|
}
|
|
128
170
|
}
|
|
129
171
|
|
|
130
172
|
// -------------------------------------------------------------
|
|
131
173
|
// GATE 1: Hard Touchpoint Lock (if features are defined)
|
|
132
174
|
// -------------------------------------------------------------
|
|
133
|
-
const features = config.features ?? {};
|
|
134
175
|
const session = SessionManager.getActiveSession(process.cwd());
|
|
135
|
-
const
|
|
136
|
-
|
|
176
|
+
const requestedFeatureKey = toolInput.feature_key || process.env.SEPTUM_ACTIVE_FEATURE;
|
|
177
|
+
if (session && requestedFeatureKey && requestedFeatureKey !== session.feature_key) {
|
|
178
|
+
throw new Error(`Requested feature '${requestedFeatureKey}' does not own the active scope '${session.feature_key}'.`);
|
|
179
|
+
}
|
|
180
|
+
const activeFeatureKey = session?.feature_key || requestedFeatureKey;
|
|
137
181
|
|
|
138
|
-
if (activeFeatureKey
|
|
182
|
+
if (activeFeatureKey) {
|
|
139
183
|
const touchpointViolations = evaluator.checkFeatureTouchpoints(
|
|
140
184
|
activeFeatureKey,
|
|
141
185
|
targetFile,
|
|
@@ -143,14 +187,13 @@ async function runPreWriteHook(): Promise<void> {
|
|
|
143
187
|
);
|
|
144
188
|
|
|
145
189
|
if (touchpointViolations.length > 0) {
|
|
146
|
-
|
|
190
|
+
blockHook(
|
|
191
|
+
db,
|
|
147
192
|
`\n[SEPTUM GATE 1 VIOLATION - Scope Touchpoint Lock]\n` +
|
|
148
193
|
`✕ Operation blocked: File '${targetFile}' is outside the declared allowed_touchpoints for feature '${activeFeatureKey}'.\n` +
|
|
149
194
|
`▶ REASON: ${touchpointViolations[0].message}\n` +
|
|
150
195
|
`▶ REQUIRED ACTION: Only modify files declared in allowed_touchpoints for this feature.\n`
|
|
151
196
|
);
|
|
152
|
-
db.close();
|
|
153
|
-
process.exit(1);
|
|
154
197
|
}
|
|
155
198
|
}
|
|
156
199
|
|
|
@@ -161,6 +204,9 @@ async function runPreWriteHook(): Promise<void> {
|
|
|
161
204
|
if (content && sourceDomain) {
|
|
162
205
|
const astEngine = new ASTParserEngine();
|
|
163
206
|
const parsed = await astEngine.parseFile(targetFile, content);
|
|
207
|
+
if (ASTParserEngine.isSourceFile(targetFile) && (parsed.status === "unsupported" || parsed.status === "degraded")) {
|
|
208
|
+
throw new Error(`Cannot reliably check imports in '${targetFile}': parser status is ${parsed.status}.`);
|
|
209
|
+
}
|
|
164
210
|
|
|
165
211
|
const proposedImports = parsed.dependencies.map((d) => d.target);
|
|
166
212
|
const boundaryViolations = evaluator.checkProposedChanges(
|
|
@@ -171,7 +217,8 @@ async function runPreWriteHook(): Promise<void> {
|
|
|
171
217
|
|
|
172
218
|
if (boundaryViolations.length > 0) {
|
|
173
219
|
const v = boundaryViolations[0];
|
|
174
|
-
|
|
220
|
+
blockHook(
|
|
221
|
+
db,
|
|
175
222
|
`\n[SEPTUM GATE 2 VIOLATION - Forbidden Cross-Domain Import]\n` +
|
|
176
223
|
`✕ Operation blocked: Proposed code imports forbidden domain '${v.target_domain}' from '${v.source_domain}'.\n` +
|
|
177
224
|
`▶ OFFENDING IMPORT: ${v.imported_target}\n` +
|
|
@@ -179,12 +226,17 @@ async function runPreWriteHook(): Promise<void> {
|
|
|
179
226
|
`▶ MESSAGE: ${v.message}\n` +
|
|
180
227
|
`▶ REQUIRED ACTION: Decouple this dependency. Use an authorized Interface, Event, or Shared Kernel.\n`
|
|
181
228
|
);
|
|
182
|
-
db.close();
|
|
183
|
-
process.exit(1);
|
|
184
229
|
}
|
|
185
230
|
}
|
|
186
231
|
|
|
187
232
|
db.close();
|
|
233
|
+
|
|
234
|
+
// If running in Antigravity or standard JSON IPC mode, emit allow decision
|
|
235
|
+
try {
|
|
236
|
+
process.stdout.write(JSON.stringify({ decision: "allow" }) + "\n");
|
|
237
|
+
} catch {
|
|
238
|
+
// ignore
|
|
239
|
+
}
|
|
188
240
|
process.exit(0);
|
|
189
241
|
} catch (err) {
|
|
190
242
|
db.close();
|