twining_why
Check what decisions constrain a file before modifying it. View rationale and alternatives to avoid contradicting prior choices.
Instructions
Before modifying a file, check what decisions constrain it. Shows rationale and alternatives so you don't contradict prior choices.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| scope | Yes | File path, module name, or symbol to query |
Implementation Reference
- src/tools/decision-tools.ts:113-136 (registration)Registers the 'twining_why' MCP tool with scope input and delegates to engine.why()
// twining_why — Retrieve decision chain for a scope or file server.registerTool( "twining_why", { description: "Before modifying a file, check what decisions constrain it. Shows rationale and alternatives so you don't contradict prior choices.", inputSchema: { scope: z .string() .describe("File path, module name, or symbol to query"), }, }, async (args) => { try { const result = await engine.why(args.scope); return toolResult(result); } catch (e) { return toolError( e instanceof Error ? e.message : "Unknown error", "INTERNAL_ERROR", ); } }, ); - src/engine/decisions.ts:312-346 (handler)Core handler logic: queries DecisionStore.getByScope(scope) then maps results to return decisions with rationale, alternatives count, and active/provisional counts
/** Retrieve decision chain for a scope or file. */ async why(scope: string): Promise<{ decisions: Array<{ id: string; summary: string; rationale: string; confidence: string; status: string; timestamp: string; alternatives_count: number; commit_hashes: string[]; }>; active_count: number; provisional_count: number; }> { const decisions = await this.decisionStore.getByScope(scope); const mapped = decisions.map((d) => ({ id: d.id, summary: d.summary, rationale: d.rationale, confidence: d.confidence, status: d.status, timestamp: d.timestamp, alternatives_count: d.alternatives.length, commit_hashes: d.commit_hashes ?? [], })); const active_count = decisions.filter((d) => d.status === "active").length; const provisional_count = decisions.filter( (d) => d.status === "provisional", ).length; return { decisions: mapped, active_count, provisional_count }; } - src/storage/decision-store.ts:73-102 (helper)Storage helper that queries the decision index by scope prefix-match, affected files, or affected symbols, then loads full decision JSON files
/** Get all decisions matching a scope (prefix match or affected files/symbols match). */ async getByScope(scope: string): Promise<Decision[]> { const index = await this.getIndex(); const matching = index.filter( (entry) => entry.scope.startsWith(scope) || scope.startsWith(entry.scope) || entry.affected_files.some( (f) => f.startsWith(scope) || scope.startsWith(f), ) || entry.affected_symbols.some((s) => s === scope), ); // Load full decision files for matches const decisions: Decision[] = []; for (const entry of matching) { const decision = await this.get(entry.id); if (decision) decisions.push(decision); } // Sort by timestamp descending, then by ID descending (ULID is monotonic) decisions.sort( (a, b) => b.timestamp.localeCompare(a.timestamp) || b.id.localeCompare(a.id), ); return decisions; }