(filePath: string, content: string)
| 67 | private toolCallId?: string, |
| 68 | ) {} |
| 69 | |
| 70 | async write(filePath: string, content: string): Promise<void> { |
| 71 | this.ensureOpen(); |
| 72 | const abs = path.resolve(filePath); |
| 73 | |
| 74 | // ─── Autonomy-contract scope gate (--scope) ─── |
| 75 | // When a headless contract run pins a write scope, edits outside it are refused |
| 76 | // BEFORE anything touches disk. No-op unless a scope root is registered (see |
| 77 | // autonomy-contract.ts). Rollback/undo paths write via fs directly — never gated. |
| 78 | { |
| 79 | const denial = checkWriteScope(abs); |
| 80 | if (denial) { |
| 81 | logger.info('Scope gate refused write', { txnId: this.id, path: abs }); |
| 82 | throw new Error(denial); |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | let beforeContent: string | null = null; |
| 87 | let beforeHash: string | null = null; |
| 88 | let isCreate = false; |
| 89 | |
| 90 | try { |
| 91 | beforeContent = await fs.readFile(abs, 'utf-8'); |
| 92 | beforeHash = hashContent(beforeContent); |
| 93 | } catch (e: any) { |
| 94 | if (e.code === 'ENOENT') { |
| 95 | isCreate = true; |
| 96 | } else { |
| 97 | throw e; |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | // ─── Pre-commit syntax gate ─── |
| 102 | // Parse the candidate content (tree-sitter, in-process) BEFORE it touches the |
| 103 | // disk. Refuses only when the edit would turn a clean-parsing file into a broken |
| 104 | // one (baseline tolerance) — see syntax-check.ts. The throw surfaces to the model |
| 105 | // as a [SYNTAX_REJECTED] tool-result observation; the file on disk stays intact. |
| 106 | // Rollback/undo paths use fs.writeFile directly and are NOT gated. |
| 107 | // Off-switch: discipline.syntaxGate: false |
| 108 | { |
| 109 | const { checkSyntaxForWrite } = await import('../tools/ast/syntax-check.js'); |
| 110 | const rejection = await checkSyntaxForWrite(abs, beforeContent, content); |
| 111 | if (rejection) { |
| 112 | logger.info('Syntax gate refused write', { txnId: this.id, path: abs }); |
| 113 | throw new Error(rejection); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | if (beforeContent !== null) { |
| 118 | await this.journal.storeBlob(beforeHash!, beforeContent); |
| 119 | } |
| 120 | |
| 121 | const afterHash = hashContent(content); |
| 122 | await this.journal.storeBlob(afterHash, content); |
| 123 | |
| 124 | await fs.mkdir(path.dirname(abs), { recursive: true }); |
| 125 | await writeFileAtomic(abs, content, { encoding: 'utf-8' }); |
| 126 |
nothing calls this directly
no test coverage detected