diff --git a/docs/agents/plans/agents-doc-generation-mvp.md b/docs/agents/plans/agents-doc-generation-mvp.md new file mode 100644 index 0000000..368e544 --- /dev/null +++ b/docs/agents/plans/agents-doc-generation-mvp.md @@ -0,0 +1,1168 @@ +--- +created: 2025-11-08 +lastUpdated: 2025-11-08 +author: Generated via Amp (simplified from comprehensive plan) +status: ready +thread: https://ampcode.com/threads/T-c3ede67c-37ec-4cf8-a360-659beed5b6d1 +audience: Development team, AI coding agents, and technical decision makers +tags: [agents-md, documentation, amp-sdk, effect-wrapper, mvp] +related: + - ./agents-doc-generation.md + - ./checkpoint-based-audit-persistence.md + - ../concepts/amp-integration.md + - ../../AGENTS.md +--- + +# Norms Capture MVP: Amp SDK + Effect + +## Executive Summary + +Capture directory-specific migration norms from checkpoint history and document them in AGENTS.md files. This MVP wraps Amp's TypeScript SDK in Effect services to leverage Amp's capabilities for documentation generation and thread analysis. + +**Key Innovation:** Use Effect to compose checkpoint analysis with Amp SDK calls, automatically capturing the norms that emerged during migration work—creating shared agreements between humans and agents about how code should be written. + +**Effort:** 2-4 hours (vs 12-20 hours for full deterministic approach) + +**Trade-offs:** +- ✅ Much simpler implementation +- ✅ Better quality docs (Amp writes better than templates) +- ✅ Dogfoods Effect patterns (wrapping external SDKs) +- ❌ Requires Amp API access (not standalone) +- ❌ Less deterministic (AI can vary) + +--- + +## What This Feature Does + +As you migrate directories to Effect, `effect-migrate` tracks your progress via checkpoints. This feature automatically generates **directory-specific AGENTS.md files** that document the norms and patterns established during migration. + +**Example:** + +After migrating `src/services/` over several Amp sessions, the checkpoint history shows: +- All async/await replaced with Effect.gen +- All Node.js imports replaced with @effect/platform +- All console.* calls replaced with Effect Console service + +Instead of manually documenting these norms, run: + +```bash +effect-migrate norms capture +``` + +This analyzes checkpoint history and captures the norms in `src/services/AGENTS.md`: + +````markdown +--- +generated: 2025-11-08T14:30:00Z +status: migrated +--- + +# AGENTS for src/services + +## Enforced Norms + +- **no-async-await** — Use Effect.gen instead +- **no-node-imports** — Use @effect/platform APIs +- **use-effect-console** — Use Console service + +## Migration History + +- Thread T-abc123 (2025-11-03): Migrated async patterns +- Thread T-def456 (2025-11-05): Enforced platform boundaries +```` + +Now when Amp works in `src/services/`, it automatically loads this AGENTS.md and knows to follow these norms. + +### Three Usage Modes + +**1. Fully Automated (2-4 hour implementation)** + +```bash +effect-migrate norms capture +# ✓ Analyzes checkpoint history +# ✓ Calls Amp SDK to document norms +# ✓ Writes AGENTS.md files +``` + +**2. Prepare + Manual (1 hour implementation)** + +```bash +effect-migrate norms capture --prepare-only +# ✓ Writes norm summaries to .amp/effect-migrate/norms/ + +# Then in Amp: +# "Read @.amp/effect-migrate/norms/ and document these in AGENTS.md for each directory" +``` + +**3. From Within Amp (recommended)** + +``` +You: "Run effect-migrate norms capture for migrated directories" +Amp: [executes command with API key already available] + [captures norms and writes AGENTS.md files] +``` + +--- + +## Problem Statement + +As teams migrate directories to Effect, checkpoint history reveals which norms emerged from actual work. We want to automatically capture these norms and document them in AGENTS.md files—creating shared agreements between humans and agents. + +**Instead of building complex templating infrastructure, let Amp do what it's good at: understanding context and writing documentation.** + +--- + +## Architecture + +> **🤔 EVEN SIMPLER ALTERNATIVE:** +> +> Since Amp is likely executing this command, we could skip the SDK entirely and use **Amp's built-in tools**: +> +> ```typescript +> // Instead of calling Amp SDK from within effect-migrate... +> const content = yield* amp.executePrompt(prompt) +> +> // Just capture the norms and let Amp document them: +> const norms = yield* captureNorms(directory) +> yield* writeFile(".amp/effect-migrate/norms/src-services.json", norms) +> +> // Then output a message telling Amp what to do: +> yield* Console.log(` +> ✓ Norms captured to .amp/effect-migrate/norms/src-services.json +> +> To document in AGENTS.md: +> Read @.amp/effect-migrate/norms/src-services.json and document in ${directory}/AGENTS.md +> `) +> ``` +> +> **Why this might be better:** +> - Even simpler (no SDK dependency) +> - Amp can use its full context (files, git, etc.) +> - User can review/tweak before generation +> - Amp handles the actual writing +> +> **Why we use SDK instead:** +> - Automated workflow (no user intervention) +> - Programmatic composition within Effect +> - Better for batch processing multiple directories +> - Effect error handling and retries +> +> **Decision:** Use SDK for automation, but could add `--prepare-only` flag for manual workflow. + +### Data Flow + +```text +Checkpoint History + │ + ▼ +Directory Summarizer (Effect) + │ + ├─ Query latest checkpoint + ├─ Group findings by directory + ├─ Identify established norms + └─ Extract thread associations + │ + ▼ +JSON Summary Output + │ + ▼ +Amp Service (Effect-wrapped SDK) + │ + ├─ Load summary context + ├─ Read associated threads + └─ Generate AGENTS.md content + │ + ▼ +FileSystem Service (Effect) + │ + └─ Write directory/AGENTS.md +``` + +### Service Architecture + +```typescript +// Core services we need +interface DirectorySummarizer { + summarize(directory: string): Effect +} + +interface AmpService { + executePrompt(prompt: string, context?: string[]): Effect + readThread(threadId: string): Effect +} + +// Composition +const generateAgentsMd = (directory: string) => + Effect.gen(function* () { + const summarizer = yield* DirectorySummarizer + const amp = yield* AmpService + const fs = yield* FileSystem.FileSystem + + // 1. Prepare checkpoint summary + const summary = yield* summarizer.summarize(directory) + + // 2. Generate docs with Amp + const content = yield* amp.executePrompt( + `Generate AGENTS.md for ${directory} based on this migration history`, + [JSON.stringify(summary)] + ) + + // 3. Write to filesystem + yield* fs.writeFileString(`${directory}/AGENTS.md`, content) + }) +``` + +--- + +## Implementation + +### Phase 1: Directory Summarizer (1-2 hours) + +**Goal:** Extract key migration metrics from checkpoint data. + +#### DirectorySummary Type + +```typescript +import * as Schema from "effect/Schema" + +export const DirectorySummary = Schema.Struct({ + /** Directory path relative to project root */ + directory: Schema.String, + + /** Migration status */ + status: Schema.Literal("migrated", "in-progress", "not-started"), + + /** When directory became clean (if applicable) */ + cleanSince: Schema.optional(Schema.DateTimeUtc), + + /** File statistics */ + files: Schema.Struct({ + total: Schema.Number, + clean: Schema.Number, + withViolations: Schema.Number + }), + + /** Norms established (rules that went to zero) */ + norms: Schema.Array( + Schema.Struct({ + ruleId: Schema.String, + ruleKind: Schema.String, + severity: Schema.String, + message: Schema.String, + docsUrl: Schema.optional(Schema.String), + fixedCount: Schema.Number, + lastSeenAt: Schema.DateTimeUtc, + extinctSince: Schema.DateTimeUtc + }) + ), + + /** Associated threads */ + threads: Schema.Array( + Schema.Struct({ + threadId: Schema.String, + threadUrl: Schema.optional(Schema.String), + dateRange: Schema.Struct({ + first: Schema.DateTimeUtc, + last: Schema.DateTimeUtc + }), + fixesDelta: Schema.Struct({ + errors: Schema.Number, + warnings: Schema.Number + }) + }) + ), + + /** Latest checkpoint metadata */ + checkpoint: Schema.Struct({ + id: Schema.String, + timestamp: Schema.DateTimeUtc, + totalFindings: Schema.Number + }) +}) + +export type DirectorySummary = Schema.Schema.Type +``` + +#### DirectorySummarizer Service + +```typescript +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import * as Schema from "effect/Schema" +import { FileSystem } from "@effect/platform" +import * as Path from "@effect/platform/Path" + +export interface DirectorySummarizerService { + /** + * Summarize migration status for a directory. + */ + readonly summarize: ( + directory: string + ) => Effect.Effect + + /** + * List all directories that qualify for AGENTS.md generation. + */ + readonly listQualifying: (options?: { + readonly minFiles?: number + readonly status?: "migrated" | "in-progress" | "all" + }) => Effect.Effect +} + +export class DirectorySummarizer extends Context.Tag("DirectorySummarizer")< + DirectorySummarizer, + DirectorySummarizerService +>() {} +``` + +#### JSON-Based Implementation + +```typescript +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { FileSystem } from "@effect/platform" +import * as Path from "@effect/platform/Path" + +/** + * JSON-based directory summarizer. + * + * Reads latest checkpoint from .amp/effect-migrate/checkpoints/ + * and extracts directory-level metrics. + */ +export const DirectorySummarizerLive = Layer.effect( + DirectorySummarizer, + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + + return DirectorySummarizer.of({ + summarize: (directory) => + Effect.gen(function* () { + // Read latest checkpoint + const manifestPath = path.join( + ".amp/effect-migrate/checkpoints", + "manifest.json" + ) + + const manifestContent = yield* fs.readFileString(manifestPath) + const manifest = JSON.parse(manifestContent) + + const latestId = manifest.latest + const latestPath = path.join( + ".amp/effect-migrate/checkpoints", + `${latestId}.json` + ) + + const checkpointContent = yield* fs.readFileString(latestPath) + const checkpoint = JSON.parse(checkpointContent) + + // Filter findings for this directory + const dirFindings = checkpoint.results.filter( + (result: any) => + checkpoint.files[result.fileIdx].startsWith(directory) + ) + + // Get unique files in directory + const dirFiles = new Set( + checkpoint.files.filter((f: string) => f.startsWith(directory)) + ) + + const filesWithViolations = new Set( + dirFindings.map((r: any) => checkpoint.files[r.fileIdx]) + ) + + // Identify norms (rules with no current violations) + const currentRules = new Set( + dirFindings.map((r: any) => checkpoint.rules[r.ruleIdx].id) + ) + + // Read previous checkpoints to find extinct rules + const previousIds = manifest.history.slice(0, 5) // Last 5 checkpoints + const norms: any[] = [] + + for (const prevId of previousIds) { + const prevPath = path.join( + ".amp/effect-migrate/checkpoints", + `${prevId}.json` + ) + const prevContent = yield* fs.readFileString(prevPath) + const prevCheckpoint = JSON.parse(prevContent) + + const prevDirFindings = prevCheckpoint.results.filter( + (result: any) => + prevCheckpoint.files[result.fileIdx].startsWith(directory) + ) + + // Rules that existed before but are gone now + const prevRules = new Map() + for (const finding of prevDirFindings) { + const rule = prevCheckpoint.rules[finding.ruleIdx] + if (!currentRules.has(rule.id)) { + prevRules.set(rule.id, { + ...rule, + count: (prevRules.get(rule.id)?.count || 0) + 1 + }) + } + } + + for (const [ruleId, ruleData] of prevRules) { + if (!norms.find((n) => n.ruleId === ruleId)) { + norms.push({ + ruleId, + ruleKind: ruleData.kind, + severity: ruleData.severity, + message: ruleData.message, + docsUrl: ruleData.docsUrl, + fixedCount: ruleData.count, + lastSeenAt: new Date(prevCheckpoint.timestamp), + extinctSince: new Date(checkpoint.timestamp) + }) + } + } + } + + // Extract thread associations + const threads = manifest.history + .filter((entry: any) => entry.threadId) + .map((entry: any) => ({ + threadId: entry.threadId, + threadUrl: entry.threadUrl, + dateRange: { + first: new Date(entry.timestamp), + last: new Date(entry.timestamp) + }, + fixesDelta: { + errors: 0, // Would need delta calculation + warnings: 0 + } + })) + + // Determine status + const status: "migrated" | "in-progress" | "not-started" = + dirFindings.length === 0 && norms.length > 0 + ? "migrated" + : dirFindings.length > 0 + ? "in-progress" + : "not-started" + + return { + directory, + status, + cleanSince: status === "migrated" ? new Date(checkpoint.timestamp) : undefined, + files: { + total: dirFiles.size, + clean: dirFiles.size - filesWithViolations.size, + withViolations: filesWithViolations.size + }, + norms, + threads, + checkpoint: { + id: latestId, + timestamp: new Date(checkpoint.timestamp), + totalFindings: dirFindings.length + } + } + }), + + listQualifying: (options = {}) => + Effect.gen(function* () { + const { minFiles = 3, status = "all" } = options + + // Read latest checkpoint to get all directories + const manifestPath = path.join( + ".amp/effect-migrate/checkpoints", + "manifest.json" + ) + + const manifestContent = yield* fs.readFileString(manifestPath) + const manifest = JSON.parse(manifestContent) + + const latestId = manifest.latest + const latestPath = path.join( + ".amp/effect-migrate/checkpoints", + `${latestId}.json` + ) + + const checkpointContent = yield* fs.readFileString(latestPath) + const checkpoint = JSON.parse(checkpointContent) + + // Extract unique directories (depth 2) + const directories = new Set() + for (const file of checkpoint.files) { + const parts = file.split('/') + if (parts.length >= 2) { + directories.add(`${parts[0]}/${parts[1]}`) + } + } + + // Summarize each directory + const summaries = yield* Effect.forEach( + Array.from(directories), + (dir) => this.summarize(dir), + { concurrency: 4 } + ) + + // Filter by criteria + return summaries.filter( + (summary) => + summary.files.total >= minFiles && + (status === "all" || summary.status === status) + ) + }) + }) + }) +) +``` + +--- + +### Phase 2: Amp SDK Service (1-2 hours) + +**Goal:** Wrap Amp TypeScript SDK in Effect service for composability. + +#### AmpService Interface + +```typescript +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import * as Data from "effect/Data" + +/** + * Amp SDK errors. + */ +export class AmpError extends Data.TaggedError("AmpError")<{ + readonly message: string + readonly cause?: unknown +}> {} + +export interface ThreadContent { + readonly id: string + readonly messages: readonly { + readonly role: "user" | "assistant" + readonly content: string + }[] + readonly files?: readonly string[] +} + +export interface AmpServiceInterface { + /** + * Execute a prompt and return the result. + */ + readonly executePrompt: ( + prompt: string, + options?: { + readonly context?: readonly string[] + readonly timeout?: number + } + ) => Effect.Effect + + /** + * Read thread content by ID. + */ + readonly readThread: ( + threadId: string + ) => Effect.Effect +} + +export class AmpService extends Context.Tag("AmpService")< + AmpService, + AmpServiceInterface +>() {} +``` + +#### SDK Implementation + +```typescript +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Config from "effect/Config" +import * as Duration from "effect/Duration" +import { AmpClient } from "@sourcegraph/amp" + +/** + * Live Amp service using @sourcegraph/amp SDK. + * + * Wraps SDK methods in Effect for composability, error handling, + * and resource management. + */ +export const AmpServiceLive = Layer.effect( + AmpService, + Effect.gen(function* () { + // Get API key from config + const apiKey = yield* Config.redacted("AMP_API_KEY") + + // Create client with resource cleanup + const client = yield* Effect.acquireRelease( + Effect.sync(() => new AmpClient({ + apiKey: Config.Redacted.value(apiKey) + })), + (client) => Effect.sync(() => client.close?.()) + ) + + return AmpService.of({ + executePrompt: (prompt, options = {}) => + Effect.gen(function* () { + const { context = [], timeout = 30000 } = options + + // Build full prompt with context + const contextStr = context.length > 0 + ? `\n\nContext:\n${context.map((c, i) => `[${i + 1}] ${c}`).join('\n\n')}` + : '' + + const fullPrompt = `${prompt}${contextStr}` + + // Execute with timeout + const result = yield* Effect.tryPromise({ + try: () => client.execute({ + prompt: fullPrompt, + mode: 'smart' // Use smart mode for quality + }), + catch: (error) => + new AmpError({ + message: `Amp execution failed: ${String(error)}`, + cause: error + }) + }).pipe( + Effect.timeout(Duration.millis(timeout)), + Effect.catchTag("TimeoutException", () => + Effect.fail( + new AmpError({ + message: `Amp execution timed out after ${timeout}ms` + }) + ) + ) + ) + + // Extract text response + return result.response || result.text || '' + }), + + readThread: (threadId) => + Effect.gen(function* () { + const thread = yield* Effect.tryPromise({ + try: () => client.getThread(threadId), + catch: (error) => + new AmpError({ + message: `Failed to read thread ${threadId}: ${String(error)}`, + cause: error + }) + }) + + return { + id: thread.id, + messages: thread.messages.map((msg: any) => ({ + role: msg.role, + content: msg.content + })), + files: thread.files + } + }) + }) + }) +) +``` + +--- + +### Phase 3: CLI Command (30 minutes) + +**Goal:** Wire everything together in a simple CLI command. + +#### Command Implementation + +```typescript +import * as Command from "@effect/cli/Command" +import * as Options from "@effect/cli/Options" +import * as Effect from "effect/Effect" +import * as Console from "effect/Console" +import { FileSystem } from "@effect/platform" +import * as Path from "@effect/platform/Path" +import { DirectorySummarizer, AmpService } from "@effect-migrate/core" + +const normsCaptureCommand = Command.make( + "capture", + { + target: Options.choice("target", ["migrated", "in-progress", "all"]).pipe( + Options.withDefault("migrated") + ), + minFiles: Options.integer("min-files").pipe( + Options.withDefault(3) + ), + overwrite: Options.boolean("overwrite").pipe( + Options.withDefault(false) + ) + }, + (opts) => + Effect.gen(function* () { + const summarizer = yield* DirectorySummarizer + const amp = yield* AmpService + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + + yield* Console.log("🔍 Analyzing checkpoint history...") + + // Get qualifying directories + const summaries = yield* summarizer.listQualifying({ + minFiles: opts.minFiles, + status: opts.target + }) + + if (summaries.length === 0) { + yield* Console.log(`\n❌ No directories matched criteria for ${opts.target}`) + return + } + + yield* Console.log(`\n✨ Found ${summaries.length} directories to document\n`) + + // Generate AGENTS.md for each directory + const results = yield* Effect.forEach( + summaries, + (summary) => + Effect.gen(function* () { + const agentsPath = path.join(summary.directory, "AGENTS.md") + + // Check if exists + const exists = yield* fs.exists(agentsPath) + if (exists && !opts.overwrite) { + yield* Console.log( + `⏭️ Skipping ${summary.directory} (AGENTS.md exists, use --overwrite)` + ) + return { path: agentsPath, skipped: true } + } + + yield* Console.log(`📝 Generating ${agentsPath}...`) + + // Prepare context for Amp + const contextJson = JSON.stringify(summary, null, 2) + + // Build prompt for Amp + const prompt = ` +You are documenting migration norms for a TypeScript codebase being migrated to Effect. + +Generate an AGENTS.md file for the directory: ${summary.directory} + +Based on this checkpoint analysis, document: +1. Migration status (${summary.status}) +2. Enforced norms (rules that are consistently followed) +3. Thread history (migration decisions and work done) +4. Verification commands + +Use the YAML frontmatter: +--- +generated: ${new Date().toISOString()} +status: ${summary.status} +source: effect-migrate checkpoint analysis +--- + +Keep the tone technical and concise. Focus on actionable guidance for AI coding agents. +Format as Markdown with proper headings and code blocks. + `.trim() + + // Generate with Amp + const content = yield* amp.executePrompt(prompt, { + context: [contextJson], + timeout: 45000 + }) + + // Write to filesystem + yield* fs.writeFileString(agentsPath, content) + + yield* Console.log(` ✅ ${agentsPath}`) + + return { path: agentsPath, skipped: false } + }), + { concurrency: 1 } // Sequential to avoid rate limits + ) + + const generated = results.filter((r) => !r.skipped) + + yield* Console.log(`\n✨ Generated ${generated.length} AGENTS.md files`) + yield* Console.log( + `\n💡 Tip: Amp will auto-load these files when working in these directories` + ) + }) +) + +export const normsCommand = Command.make("norms").pipe( + Command.withSubcommands([normsCaptureCommand]) +) +``` + +--- + +## Usage + +### Installation + +```bash +# Install Amp SDK +pnpm add @sourcegraph/amp +``` + +### Configuration + +```bash +# Set Amp API key (same key used for Amp sessions) +export AMP_API_KEY=your-api-key +``` + +> **💡 IMPORTANT:** If Amp is executing effect-migrate (which is the primary use case), the `AMP_API_KEY` is **already available** in Amp's environment. +> +> Users don't need to set it separately - Amp provides it automatically to any commands it runs. +> +> **Two execution modes:** +> +> 1. **From Amp session** (primary): +> ``` +> User: "Run effect-migrate agents generate for migrated directories" +> Amp: [executes command with AMP_API_KEY already set] +> ``` +> +> 2. **Standalone** (rare): +> ```bash +> # User running directly in terminal +> export AMP_API_KEY=sk-... +> effect-migrate agents generate +> ``` +> +> The SDK approach still makes sense because we're composing programmatically within the same process, not spawning Amp as a subprocess. + +### CLI Commands + +```bash +# Capture norms for all migrated directories +effect-migrate norms capture + +# Capture for all directories (including in-progress) +effect-migrate norms capture --target all + +# Customize minimum file count +effect-migrate norms capture --min-files 5 + +# Overwrite existing AGENTS.md files +effect-migrate norms capture --overwrite + +# Prepare norm summaries without writing AGENTS.md (let Amp do it) +effect-migrate norms capture --prepare-only +``` + +> **💡 SIMPLEST STARTING POINT:** +> +> The `--prepare-only` flag captures norms but doesn't call Amp SDK. Instead: +> +> ```bash +> $ effect-migrate norms capture --prepare-only +> +> ✓ Captured norms for 3 directories: +> - src/services/ → .amp/effect-migrate/norms/src-services.json +> - src/models/ → .amp/effect-migrate/norms/src-models.json +> - src/utils/ → .amp/effect-migrate/norms/src-utils.json +> +> To document in AGENTS.md, tell Amp: +> "Read the norms in @.amp/effect-migrate/norms/ and document them in +> AGENTS.md for each directory" +> ``` +> +> **Benefits:** +> - No SDK dependency (Phase 1 only) +> - User controls when documentation happens +> - Can review captured norms first +> - Amp has full context when writing +> +> **Implementation:** This is just norm capture (Phase 1) - ~1 hour effort! + +--- + +## Example Output + +**Generated `src/services/AGENTS.md`:** + +````markdown +--- +generated: 2025-11-08T14:30:00Z +status: migrated +source: effect-migrate checkpoint analysis +--- + +# AGENTS for src/services + +## Status + +**✅ Migrated** (clean since 2025-11-05) + +- **Files:** 18 total, 18 clean (100%) +- **Last checkpoint:** 2025-11-08T14:00:00Z + +## Enforced Norms + +The following patterns are consistently enforced in this directory: + +### Async/Effect Patterns + +- **no-async-await** — All async operations use `Effect.gen` instead of `async/await` + - Fixed 24 violations + - Extinct since 2025-11-05 + - [Docs](https://effect.website/docs/essentials/effect-type) + +- **no-promise-constructor** — All promises wrapped in `Effect.promise` + - Fixed 15 violations + - Extinct since 2025-11-04 + +### Architectural Boundaries + +- **no-node-imports** — Use `@effect/platform` instead of Node.js built-ins + - Fixed 12 violations + - Extinct since 2025-11-05 + - [Docs](https://effect.website/docs/guides/platform) + +- **no-import-from-legacy** — Clean separation from legacy code + - Fixed 8 violations + - Extinct since 2025-11-04 + +### Service Patterns + +- **use-effect-console** — Use `Console` service instead of `console.*` + - Fixed 18 violations + - Extinct since 2025-11-03 + +## Migration History + +Key Amp threads that established these norms: + +- **T-a96b5e77** (2025-11-03 → 2025-11-05): Migrated core services to Effect patterns +- **T-5d39c44c** (2025-11-04 → 2025-11-05): Enforced platform boundaries + +## Verification + +Re-audit to verify compliance: + +```bash +effect-migrate audit --amp-out .amp/effect-migrate +effect-migrate checkpoints list +``` + +--- + +*Generated by effect-migrate using Amp* +```` + +--- + +## Testing Strategy + +### Unit Tests + +```typescript +import { describe, it, expect } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { DirectorySummarizer, AmpService } from "./agents" + +describe("DirectorySummarizer", () => { + it.effect("should summarize migrated directory", () => + Effect.gen(function* () { + const summarizer = yield* DirectorySummarizer + + const summary = yield* summarizer.summarize("src/services") + + expect(summary.directory).toBe("src/services") + expect(summary.status).toBe("migrated") + expect(summary.norms.length).toBeGreaterThan(0) + }).pipe(Effect.provide(DirectorySummarizerLive)) + ) +}) + +describe("AmpService", () => { + // Mock for testing + const AmpServiceMock = Layer.succeed(AmpService, { + executePrompt: (prompt) => + Effect.succeed(`# AGENTS.md\n\nGenerated from: ${prompt.slice(0, 50)}...`), + readThread: (threadId) => + Effect.succeed({ + id: threadId, + messages: [ + { role: "user", content: "Migrate services" }, + { role: "assistant", content: "Done" } + ] + }) + }) + + it.effect("should generate content via Amp", () => + Effect.gen(function* () { + const amp = yield* AmpService + + const content = yield* amp.executePrompt("Generate AGENTS.md") + + expect(content).toContain("# AGENTS.md") + }).pipe(Effect.provide(AmpServiceMock)) + ) +}) +``` + +--- + +## Advantages Over Deterministic Approach + +| Aspect | MVP (Amp SDK) | Full Deterministic | +| --------------------- | ------------------------------------ | ------------------------------------- | +| **Effort** | 2-4 hours | 12-20 hours | +| **Code** | ~300 LOC | ~2000 LOC | +| **Files** | 3 files | 15+ files | +| **Quality** | Amp writes better docs | Template-based (rigid) | +| **Flexibility** | Easy to customize prompts | Must update templates | +| **Thread awareness** | Amp can read-thread automatically | Need to parse thread content manually | +| **Testing** | Mock Amp service easily | Complex template testing | +| **Maintenance** | SDK updates handled by Amp team | We maintain all logic | +| **Standalone** | ❌ Requires Amp | ✅ Works offline | +| **Deterministic** | ❌ AI can vary | ✅ Same input = same output | + +--- + +## When NOT to Use This Approach + +Consider the full deterministic approach ([agents-doc-generation.md](./agents-doc-generation.md)) if: + +- **Offline operation required**: Users need to work without Amp API access +- **Bit-for-bit reproducibility**: CI/CD requires identical output every time +- **High volume**: Generating docs for 100+ directories (API rate limits) +- **Custom formats**: Need very specific doc structure that prompts can't achieve +- **No Amp dependency**: Tool must work standalone without external services + +--- + +## Migration Path + +### From This MVP + +If you start here and later need deterministic generation: + +1. Extract template from Amp-generated examples +2. Implement `TemplateDocGenerator` service +3. Add `--use-amp` flag (default false) +4. MVP becomes the "enhanced" option + +### To This MVP + +If you've already built deterministic infrastructure: + +1. Add `AmpService` wrapper +2. Add `--use-amp` flag (optional) +3. Compare quality of Amp vs template output +4. Decide which to make default + +--- + +## File Structure + +``` +packages/core/src/ +├── amp/ +│ └── AmpService.ts # Effect wrapper for @sourcegraph/amp SDK +└── norms/ + └── NormCapture.ts # Checkpoint → norm extraction + +packages/cli/src/ +└── commands/ + └── norms.ts # CLI command implementation + +packages/core/test/ +└── norms/ + ├── NormCapture.test.ts + └── AmpService.test.ts +``` + +--- + +## Dependencies + +```json +{ + "dependencies": { + "@sourcegraph/amp": "^0.1.0", + "effect": "^3.19.2", + "@effect/platform": "^0.93.0", + "@effect/cli": "^0.72.0" + } +} +``` + +--- + +## Success Criteria + +- [ ] DirectorySummarizer extracts accurate metrics from checkpoints +- [ ] AmpService wraps SDK with proper error handling and timeouts +- [ ] CLI command generates readable AGENTS.md files +- [ ] Generated docs include status, norms, threads, verification +- [ ] Amp's output is high quality and contextually relevant +- [ ] Tests cover service mocking and integration +- [ ] Performance: <5s per directory (including Amp API latency) + +--- + +## Performance Targets + +| Metric | Target | Notes | +| -------------------------- | ----------------------- | ------------------------------ | +| Summary extraction | <200ms per directory | JSON parsing + filtering | +| Amp API call | <10s per directory | Smart mode with timeout | +| File write | <50ms | Native filesystem | +| Total per directory | <15s | Including Amp generation | +| Sequential generation (5) | <1 minute | Rate limit friendly | + +--- + +## Future Enhancements + +### Caching + +```typescript +// Cache Amp responses to avoid regeneration +const withCache = Effect.cachedWithTTL( + amp.executePrompt, + Duration.hours(24) +) +``` + +### Batch Processing + +```typescript +// Generate multiple directories in parallel (with rate limiting) +Effect.forEach( + directories, + (dir) => generateAgentsMd(dir), + { concurrency: 2 } // Respect API limits +) +``` + +### Custom Prompts + +```typescript +// Allow users to customize generation prompts +const customPrompt = fs.readFileString(".amp/agents-prompt.txt") +amp.executePrompt(customPrompt, { context: [summary] }) +``` + +### Diff Mode + +```typescript +// Show what changed since last generation +const existingContent = fs.readFileString(`${dir}/AGENTS.md`) +const newContent = amp.executePrompt(...) +const diff = computeDiff(existingContent, newContent) +``` + +--- + +## Conclusion + +This MVP approach leverages Amp's strengths while dogfooding Effect patterns. It's the simplest path to valuable functionality, with clear upgrade paths if deterministic generation becomes necessary. + +**Recommended first step:** Prototype `DirectorySummarizer` and validate that checkpoint data contains sufficient information for meaningful AGENTS.md generation. + +--- + +**Last Updated:** 2025-11-08 +**Maintainer:** Ari Dyckovsky +**Status:** Ready for implementation +**Next Steps:** Prototype DirectorySummarizer, validate summary schema with team, implement AmpService wrapper diff --git a/docs/agents/plans/agents-doc-generation.md b/docs/agents/plans/agents-doc-generation.md new file mode 100644 index 0000000..e548f6e --- /dev/null +++ b/docs/agents/plans/agents-doc-generation.md @@ -0,0 +1,2066 @@ +--- +created: 2025-11-08 +lastUpdated: 2025-11-08 +author: Generated via Amp (Oracle + Librarian comprehensive research) +status: ready +thread: https://ampcode.com/threads/T-c3ede67c-37ec-4cf8-a360-659beed5b6d1 +audience: Development team, AI coding agents, and technical decision makers +tags: [agents-md, documentation, checkpoint-analysis, ai-generation, amp-integration] +related: + - ./comprehensive-data-architecture.md + - ./checkpoint-based-audit-persistence.md + - ./schema-versioning-and-normalization.md + - ../concepts/amp-integration.md + - ../../AGENTS.md +--- + +# Directory-Specific AGENTS.md Generation from Checkpoint History + +## Executive Summary + +Generate directory-specific AGENTS.md files that document migration norms, conventions, and patterns discovered through checkpoint analysis. As teams migrate to Effect using effect-migrate, the checkpoint history reveals which rules became enforced, which patterns were adopted, and which boundaries stabilized—this feature automatically captures that knowledge as AGENTS.md files for migrated directories. + +**Key Innovation:** Transform migration audit history into actionable documentation that helps Amp coding agents understand local norms in each directory without re-learning them. + +**Effort:** 12-20 hours across 3 phases (independently deliverable) + +**Dependencies:** + +- Phase 2 (Checkpoints) of comprehensive-data-architecture.md +- SQLite storage (Phase 3) recommended but not required +- @effect/ai for optional LLM enhancement + +--- + +> **💡 SIMPLIFICATION NOTE:** +> +> **Alternative Approach (Much Simpler):** Instead of building all the infrastructure below, we could: +> +> 1. Add a simple CLI command that prepares checkpoint summary context +> 2. Use Amp CLI programmatically to generate the AGENTS.md +> 3. Let Amp do the analysis, summarization, and writing +> +> ```bash +> # Simple version - leverage Amp directly +> effect-migrate agents prepare src/services/ # Creates context summary +> amp --execute "Read checkpoint summary and generate AGENTS.md for src/services/" +> ``` +> +> **Pros:** +> - Much less code to maintain (maybe 2-4 hours instead of 12-20) +> - Leverages Amp's existing strengths (analysis, summarization) +> - More flexible (Amp can adapt to context better than templates) +> - Uses same AI token user already has +> +> **Cons:** +> - Requires Amp API access (not standalone) +> - Less deterministic (AI can vary) +> - Slower (Amp execution vs direct generation) +> +> **Question for team:** Do we need the full deterministic infrastructure, or should we start with "prepare context + invoke Amp"? + +--- + +## Problem Context + +### Current State + +**What we have:** + +- Checkpoint-based audit persistence tracking migration over time +- Normalized schema reducing duplication and memory overhead +- Thread associations linking audits to Amp sessions +- Analytics capabilities for trend analysis + +**What's missing:** + +- No automatic documentation of "what norms were established" +- Amp agents must re-learn directory conventions in each session +- Migration decisions captured in threads but not codified +- No way to say "this directory is fully migrated, follow these patterns" + +### User Story + +```bash +# Team migrates src/services/ over 2 weeks, 15 checkpoints +$ effect-migrate audit --amp-out .amp/effect-migrate --checkpoint-db checkpoints.db +# ... fix issues, run audit again ... +# ... repeat until src/services/ is clean ... + +# After src/services/ stabilizes (clean for 5+ checkpoints): +$ effect-migrate agents generate --target migrated --llm threads + +✓ Generated AGENTS.md for 3 directories: + - src/services/AGENTS.md (18 files, 100% clean since 2025-11-05) + - src/models/AGENTS.md (12 files, 100% clean since 2025-11-03) + - src/utils/AGENTS.md (8 files, 100% clean since 2025-11-06) + +# Now new Amp sessions automatically load these norms +$ amp +> Amp reads src/services/AGENTS.md automatically +> Knows: no async/await, use @effect/platform, Effect.gen patterns enforced +``` + +### Value Proposition + +**For teams:** + +- ✅ Automated documentation of migration decisions +- ✅ Reduced onboarding time for new developers/agents +- ✅ Consistent patterns enforced across team +- ✅ Historical provenance of why norms exist + +**For Amp agents:** + +- ✅ Directory-specific context without manual explanation +- ✅ Clear boundaries and enforced patterns +- ✅ Thread references for deeper understanding +- ✅ Verification commands to re-check compliance + +--- + +## Architecture Overview + +> **💭 QUESTION:** Do we really need all these services and abstractions? +> +> Most of this complexity exists to avoid calling Amp. But if we're already using Amp for migrations, why not use it for generation too? +> +> **Simpler alternative architecture:** +> ```text +> CLI Command → Prepare Context Summary → Amp CLI (--execute) → Write AGENTS.md +> ``` +> +> The complex architecture below makes sense if: +> - We want standalone operation (no Amp dependency) +> - We need reproducible/deterministic output +> - We're processing hundreds of directories (perf matters) +> +> Otherwise, "context prep + Amp" is much simpler. + +### Three-Tier Generation Strategy + +```text +┌─────────────────────────────────────────────────────────────┐ +│ CLI Command Layer │ +│ effect-migrate agents generate [options] │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ AgentsInsights Engine │ +│ (Analyzes checkpoint history, classifies directories) │ +│ │ +│ • classifyDirectories() → DirectoryStatus[] │ +│ • directoryNorms() → NormEvidence[] │ +│ • directoryThreads() → ThreadImpact[] │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌──────────────┴──────────────┐ + ▼ ▼ +┌──────────────────────┐ ┌──────────────────────┐ +│ AgentsDocGenerator │ │ Optional AI Layer │ +│ (Templates + Render) │ │ (@effect/ai) │ +│ │ │ │ +│ • generateForDir() │◄───┤ • Thread summaries │ +│ • renderTemplate() │ │ • Pattern extraction│ +└──────────────────────┘ └──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Data Sources │ +│ │ +│ CheckpointStore ──► JSON/SQLite │ +│ AnalyticsEngine ──► Polars (optional) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Data Flow + +``` +Checkpoint History (50+ audits) + │ + ├─► Directory Grouping (src/services/, src/models/, etc.) + │ + ├─► Stability Analysis + │ └─ Which directories stayed clean for N checkpoints? + │ + ├─► Norm Extraction + │ └─ Which rules went to zero and stayed zero? + │ + ├─► Thread Association + │ └─ Which threads contributed to cleaning this directory? + │ + ├─► Optional AI Enhancement + │ └─ Summarize thread decisions and patterns + │ + └─► Template Rendering + └─ Generate src/services/AGENTS.md +``` + +--- + +## Phase 1: Deterministic AGENTS.md Generation (Core) + +> **🤔 SIMPLER OPTION:** Before building all this, consider: +> +> **"Context Prep" Approach (2-3 hours):** +> ```typescript +> // Just prepare a simple JSON summary +> const summary = { +> directory: "src/services/", +> status: "migrated", +> cleanSince: "2025-11-05", +> normsEstablished: [ +> { rule: "no-async-await", fixedCount: 24, docsUrl: "..." }, +> { rule: "no-node-imports", fixedCount: 12, docsUrl: "..." } +> ], +> threads: ["T-abc...", "T-def..."] +> } +> // Write to .amp/effect-migrate/directory-summary.json +> ``` +> +> Then: +> ```bash +> amp --execute "Read @.amp/effect-migrate/directory-summary.json and generate src/services/AGENTS.md documenting these norms" +> ``` +> +> **Why this might be better:** +> - Amp already knows how to write good documentation +> - Can reference thread content directly via read-thread +> - Adapts to context better than rigid templates +> - Much less code to maintain +> +> **When to use full deterministic approach:** +> - Need to work offline/without Amp +> - Processing 100+ directories (perf critical) +> - Want bit-for-bit reproducible output + +**Goal:** Generate AGENTS.md files from checkpoint data using deterministic heuristics (no LLM required). + +**Effort:** 6-10 hours + +### Core Services + +#### 1. AgentsInsights Interface + +```typescript +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import type { PlatformError } from "@effect/platform/Error" + +/** + * Directory migration status classification. + */ +export interface DirectoryStatus { + /** Directory path relative to project root */ + readonly path: string + + /** Migration status based on checkpoint stability */ + readonly status: "migrated" | "in-progress" | "not-started" + + /** When directory first became clean (all findings resolved) */ + readonly cleanSince?: Date + + /** Number of consecutive checkpoints with zero findings */ + readonly stabilityCheckpoints: number + + /** File statistics */ + readonly fileCounts: { + readonly total: number + readonly clean: number + readonly cleanPercentage: number + } + + /** Last checkpoint with changes in this directory */ + readonly lastActivity: Date +} + +/** + * Evidence of an established norm (rule that became enforced). + */ +export interface NormEvidence { + /** Rule ID that became enforced */ + readonly ruleId: string + + /** Rule kind (pattern, boundary, etc.) */ + readonly ruleKind: string + + /** Rule tags for categorization */ + readonly tags: readonly string[] + + /** Documentation URL for the rule */ + readonly docsUrl?: string + + /** When this rule last had findings in the directory */ + readonly lastSeenAt: Date + + /** When rule findings went to zero (and stayed there) */ + readonly extinctSince: Date + + /** Stability metrics */ + readonly zeroSpan: { + readonly checkpoints: number + readonly days: number + } + + /** Total findings fixed for this rule */ + readonly totalFixed: number +} + +/** + * Thread impact on a directory's migration. + */ +export interface ThreadImpact { + /** Thread ID (T-uuid format) */ + readonly threadId: string + + /** Thread URL if available */ + readonly threadUrl?: string + + /** First checkpoint in this thread */ + readonly first: Date + + /** Last checkpoint in this thread */ + readonly last: Date + + /** Delta in findings attributed to this thread */ + readonly fixesDelta: { + readonly errors: number + readonly warnings: number + } + + /** Top rules addressed in this thread */ + readonly topRules: readonly string[] +} + +/** + * Insights engine for extracting migration patterns from checkpoint history. + */ +export interface AgentsInsightsService { + /** + * Classify directories by migration status. + */ + readonly classifyDirectories: (options?: { + readonly depth?: number + readonly minFiles?: number + }) => Effect.Effect + + /** + * Extract established norms for a directory. + */ + readonly directoryNorms: ( + directory: string, + options?: { + readonly stabilityCheckpoints?: number + readonly stabilityDays?: number + readonly minHistoricalCount?: number + } + ) => Effect.Effect + + /** + * Identify threads that impacted a directory. + */ + readonly directoryThreads: ( + directory: string, + options?: { + readonly limit?: number + } + ) => Effect.Effect +} + +export class AgentsInsights extends Context.Tag("AgentsInsights")< + AgentsInsights, + AgentsInsightsService +>() {} +``` + +#### 2. AgentsDocGenerator Interface + +```typescript +/** + * Generated AGENTS.md document. + */ +export interface GeneratedAgentsDoc { + /** Directory path where AGENTS.md was written */ + readonly path: string + + /** Directory status summary */ + readonly status: DirectoryStatus + + /** Number of norms documented */ + readonly normsCount: number + + /** Number of threads referenced */ + readonly threadsCount: number +} + +/** + * Generator for directory-specific AGENTS.md files. + */ +export interface AgentsDocGeneratorService { + /** + * Generate AGENTS.md for a single directory. + */ + readonly generateForDirectory: ( + directory: string, + options?: { + readonly overwrite?: boolean + readonly includeThreads?: boolean + } + ) => Effect.Effect + + /** + * Generate AGENTS.md for all qualifying directories. + */ + readonly generateAll: (options?: { + readonly target?: "migrated" | "in-progress" | "all" + readonly depth?: number + readonly minFiles?: number + readonly stabilityCheckpoints?: number + readonly stabilityDays?: number + readonly overwrite?: boolean + }) => Effect.Effect +} + +export class AgentsDocGenerator extends Context.Tag("AgentsDocGenerator")< + AgentsDocGenerator, + AgentsDocGeneratorService +>() {} +``` + +### Implementation: SqliteAgentsInsights + +```typescript +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Schema from "effect/Schema" +import { SqlClient } from "@effect/sql" +import * as Path from "@effect/platform/Path" + +/** + * SQLite-backed insights engine. + * + * Leverages checkpoint database for efficient directory-level aggregations. + */ +export const SqliteAgentsInsightsLive = Layer.effect( + AgentsInsights, + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient + const path = yield* Path.Path + + return AgentsInsights.of({ + classifyDirectories: (options = {}) => + Effect.gen(function* () { + const { depth = 2, minFiles = 3 } = options + + // Get latest checkpoint + const latest = yield* sql<{ id: string; ts: number }>` + SELECT id, ts + FROM checkpoints + ORDER BY ts DESC + LIMIT 1 + `.pipe(Effect.map((rows) => rows[0])) + + if (!latest) { + return [] + } + + // Aggregate findings by directory at specified depth + const dirStats = yield* sql<{ + dir: string + totalFiles: number + filesWithFindings: number + totalFindings: number + errors: number + warnings: number + }>` + SELECT + -- Extract directory at specified depth + SUBSTR( + f.path, + 1, + CASE + WHEN INSTR(SUBSTR(f.path, INSTR(f.path, '/') + 1), '/') > 0 + THEN INSTR(f.path, '/') + INSTR(SUBSTR(f.path, INSTR(f.path, '/') + 1), '/') + ELSE LENGTH(f.path) + END + ) as dir, + COUNT(DISTINCT f.id) as totalFiles, + COUNT(DISTINCT CASE WHEN res.id IS NOT NULL THEN f.id END) as filesWithFindings, + COALESCE(COUNT(res.id), 0) as totalFindings, + COALESCE(SUM(CASE WHEN r.severity = 'error' THEN 1 ELSE 0 END), 0) as errors, + COALESCE(SUM(CASE WHEN r.severity = 'warning' THEN 1 ELSE 0 END), 0) as warnings + FROM files f + LEFT JOIN results res ON res.file_id = f.id AND res.checkpoint_id = ${latest.id} + LEFT JOIN rules r ON res.rule_id = r.id + GROUP BY dir + HAVING totalFiles >= ${minFiles} + ` + + // Check stability (consecutive zero findings) + const classified = yield* Effect.forEach( + dirStats, + (stat) => + Effect.gen(function* () { + // Count consecutive zero checkpoints + const stabilityData = yield* sql<{ + checkpoint_id: string + ts: number + findings: number + }>` + SELECT + c.id as checkpoint_id, + c.ts, + COUNT(res.id) as findings + FROM checkpoints c + LEFT JOIN results res ON res.checkpoint_id = c.id + LEFT JOIN files f ON res.file_id = f.id + WHERE f.path LIKE ${stat.dir + "%"} + GROUP BY c.id, c.ts + ORDER BY c.ts DESC + LIMIT 20 + ` + + // Calculate stability metrics + let stabilityCheckpoints = 0 + let cleanSince: Date | undefined = undefined + let lastActivity: Date = new Date(latest.ts * 1000) + + for (const checkpoint of stabilityData) { + if (checkpoint.findings === 0) { + stabilityCheckpoints++ + cleanSince = new Date(checkpoint.ts * 1000) + } else { + lastActivity = new Date(checkpoint.ts * 1000) + break + } + } + + // Classify status + let status: "migrated" | "in-progress" | "not-started" + if (stat.totalFindings === 0 && stabilityCheckpoints >= 3) { + status = "migrated" + } else if (stat.totalFindings > 0 || stabilityCheckpoints > 0) { + status = "in-progress" + } else { + status = "not-started" + } + + return { + path: stat.dir, + status, + cleanSince, + stabilityCheckpoints, + fileCounts: { + total: stat.totalFiles, + clean: stat.totalFiles - stat.filesWithFindings, + cleanPercentage: Math.round( + ((stat.totalFiles - stat.filesWithFindings) / stat.totalFiles) * 100 + ) + }, + lastActivity + } satisfies DirectoryStatus + }), + { concurrency: 4 } + ) + + return classified.sort((a, b) => { + // Sort: migrated first, then by clean percentage + if (a.status === "migrated" && b.status !== "migrated") return -1 + if (a.status !== "migrated" && b.status === "migrated") return 1 + return b.fileCounts.cleanPercentage - a.fileCounts.cleanPercentage + }) + }), + + directoryNorms: (directory, options = {}) => + Effect.gen(function* () { + const { stabilityCheckpoints = 3, stabilityDays = 7, minHistoricalCount = 3 } = options + + // Get rule history for this directory + const ruleHistory = yield* sql<{ + rule_id: string + kind: string + tags: string + docs_url: string | null + checkpoint_id: string + ts: number + findings: number + }>` + SELECT + r.id as rule_id, + r.kind, + r.tags, + r.docs_url, + c.id as checkpoint_id, + c.ts, + COUNT(res.id) as findings + FROM checkpoints c + CROSS JOIN rules r + LEFT JOIN results res ON res.checkpoint_id = c.id AND res.rule_id = r.id + LEFT JOIN files f ON res.file_id = f.id + WHERE f.path LIKE ${directory + "%"} OR res.id IS NULL + GROUP BY r.id, r.kind, r.tags, r.docs_url, c.id, c.ts + ORDER BY r.id, c.ts DESC + ` + + // Group by rule and analyze stability + const ruleGroups = new Map() + for (const row of ruleHistory) { + const existing = ruleGroups.get(row.rule_id) ?? [] + existing.push(row) + ruleGroups.set(row.rule_id, existing) + } + + const norms: NormEvidence[] = [] + + for (const [ruleId, checkpoints] of ruleGroups.entries()) { + // Check if rule went to zero and stayed there + let consecutiveZeros = 0 + let extinctSince: Date | undefined = undefined + let lastSeenAt: Date | undefined = undefined + let totalFixed = 0 + + for (let i = 0; i < checkpoints.length; i++) { + const checkpoint = checkpoints[i] + + if (checkpoint.findings === 0) { + consecutiveZeros++ + if (!extinctSince) { + extinctSince = new Date(checkpoint.ts * 1000) + } + } else { + // Found a non-zero checkpoint + lastSeenAt = new Date(checkpoint.ts * 1000) + totalFixed += checkpoint.findings + + // If we haven't found enough zeros, reset + if (consecutiveZeros < stabilityCheckpoints) { + consecutiveZeros = 0 + extinctSince = undefined + } + break + } + } + + // Only include if: + // 1. Rule went to zero and stayed there + // 2. Had meaningful historical count + // 3. Met stability requirements + if ( + extinctSince && + lastSeenAt && + totalFixed >= minHistoricalCount && + consecutiveZeros >= stabilityCheckpoints + ) { + const firstCheckpoint = checkpoints[0] + const daysSinceExtinct = Math.floor( + (Date.now() - extinctSince.getTime()) / (1000 * 60 * 60 * 24) + ) + + if (daysSinceExtinct >= stabilityDays) { + norms.push({ + ruleId, + ruleKind: firstCheckpoint.kind, + tags: firstCheckpoint.tags ? JSON.parse(firstCheckpoint.tags) : [], + docsUrl: firstCheckpoint.docs_url ?? undefined, + lastSeenAt, + extinctSince, + zeroSpan: { + checkpoints: consecutiveZeros, + days: daysSinceExtinct + }, + totalFixed + }) + } + } + } + + // Sort: boundaries first, then by total fixed + return norms.sort((a, b) => { + const aIsBoundary = a.tags.includes("boundary") || a.ruleKind === "boundary" + const bIsBoundary = b.tags.includes("boundary") || b.ruleKind === "boundary" + + if (aIsBoundary && !bIsBoundary) return -1 + if (!aIsBoundary && bIsBoundary) return 1 + return b.totalFixed - a.totalFixed + }) + }), + + directoryThreads: (directory, options = {}) => + Effect.gen(function* () { + const { limit = 10 } = options + + // Get checkpoints with thread associations and deltas + const threadData = yield* sql<{ + thread_id: string + thread_url: string | null + first_ts: number + last_ts: number + errors_fixed: number + warnings_fixed: number + top_rules: string + }>` + WITH dir_checkpoints AS ( + SELECT DISTINCT + c.id, + c.thread_id, + c.thread_url, + c.ts, + COUNT(CASE WHEN r.severity = 'error' THEN 1 END) as errors, + COUNT(CASE WHEN r.severity = 'warning' THEN 1 END) as warnings + FROM checkpoints c + LEFT JOIN results res ON res.checkpoint_id = c.id + LEFT JOIN files f ON res.file_id = f.id + LEFT JOIN rules r ON res.rule_id = r.id + WHERE f.path LIKE ${directory + "%"} AND c.thread_id IS NOT NULL + GROUP BY c.id, c.thread_id, c.thread_url, c.ts + ), + thread_deltas AS ( + SELECT + thread_id, + thread_url, + MIN(ts) as first_ts, + MAX(ts) as last_ts, + -- Calculate reduction in findings + ( + SELECT dc1.errors FROM dir_checkpoints dc1 + WHERE dc1.thread_id = dir_checkpoints.thread_id + ORDER BY dc1.ts ASC LIMIT 1 + ) - ( + SELECT dc2.errors FROM dir_checkpoints dc2 + WHERE dc2.thread_id = dir_checkpoints.thread_id + ORDER BY dc2.ts DESC LIMIT 1 + ) as errors_fixed, + ( + SELECT dc1.warnings FROM dir_checkpoints dc1 + WHERE dc1.thread_id = dir_checkpoints.thread_id + ORDER BY dc1.ts ASC LIMIT 1 + ) - ( + SELECT dc2.warnings FROM dir_checkpoints dc2 + WHERE dc2.thread_id = dir_checkpoints.thread_id + ORDER BY dc2.ts DESC LIMIT 1 + ) as warnings_fixed + FROM dir_checkpoints + GROUP BY thread_id, thread_url + ) + SELECT + td.*, + GROUP_CONCAT(DISTINCT r.id) as top_rules + FROM thread_deltas td + JOIN dir_checkpoints dc ON dc.thread_id = td.thread_id + JOIN results res ON res.checkpoint_id = dc.id + JOIN rules r ON res.rule_id = r.id + GROUP BY td.thread_id, td.thread_url, td.first_ts, td.last_ts, td.errors_fixed, td.warnings_fixed + ORDER BY (td.errors_fixed + td.warnings_fixed) DESC + LIMIT ${limit} + ` + + return threadData.map( + (row) => + ({ + threadId: row.thread_id, + threadUrl: row.thread_url ?? undefined, + first: new Date(row.first_ts * 1000), + last: new Date(row.last_ts * 1000), + fixesDelta: { + errors: row.errors_fixed, + warnings: row.warnings_fixed + }, + topRules: row.top_rules.split(",").slice(0, 5) + }) satisfies ThreadImpact + ) + }) + }) + }) +) +``` + +### Implementation: AgentsDocGenerator + +```typescript +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { FileSystem } from "@effect/platform" +import * as Path from "@effect/platform/Path" + +/** + * Template-based AGENTS.md generator. + * + * Renders directory-specific documentation from insights data. + */ +export const AgentsDocGeneratorLive = Layer.effect( + AgentsDocGenerator, + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const insights = yield* AgentsInsights + + const renderTemplate = ( + status: DirectoryStatus, + norms: readonly NormEvidence[], + threads: readonly ThreadImpact[] + ): string => { + const lines: string[] = [] + + // Header + lines.push(`---`) + lines.push(`generated: ${new Date().toISOString()}`) + lines.push(`status: ${status.status}`) + lines.push(`source: effect-migrate checkpoint analysis`) + lines.push(`---`) + lines.push(``) + lines.push(`# AGENTS for ${status.path}`) + lines.push(``) + + // Status section + lines.push(`## Status`) + lines.push(``) + const statusBadge = + status.status === "migrated" + ? "✅ Migrated" + : status.status === "in-progress" + ? "🔄 In Progress" + : "⏸️ Not Started" + lines.push(`**${statusBadge}**`) + lines.push(``) + + if (status.cleanSince) { + lines.push(`- **Clean since:** ${status.cleanSince.toISOString().split("T")[0]}`) + lines.push(`- **Stability:** ${status.stabilityCheckpoints} consecutive checkpoints`) + } + + lines.push( + `- **Files:** ${status.fileCounts.total} total, ${status.fileCounts.clean} clean (${status.fileCounts.cleanPercentage}%)` + ) + lines.push(`- **Last activity:** ${status.lastActivity.toISOString().split("T")[0]}`) + lines.push(``) + + // Enforced norms section + if (norms.length > 0) { + lines.push(`## Enforced Norms`) + lines.push(``) + lines.push(`The following rules are consistently enforced in this directory:`) + lines.push(``) + + // Group norms by boundary vs pattern + const boundaryNorms = norms.filter( + (n) => n.tags.includes("boundary") || n.ruleKind === "boundary" + ) + const patternNorms = norms.filter( + (n) => !n.tags.includes("boundary") && n.ruleKind !== "boundary" + ) + + if (boundaryNorms.length > 0) { + lines.push(`### Architectural Boundaries`) + lines.push(``) + for (const norm of boundaryNorms) { + lines.push( + `- **${norm.ruleId}** — Zero violations since ${norm.extinctSince.toISOString().split("T")[0]}` + ) + lines.push(` - Fixed ${norm.totalFixed} violations over ${norm.zeroSpan.days} days`) + if (norm.docsUrl) { + lines.push(` - Docs: ${norm.docsUrl}`) + } + lines.push(``) + } + } + + if (patternNorms.length > 0) { + lines.push(`### Code Patterns`) + lines.push(``) + for (const norm of patternNorms) { + const tagList = norm.tags.length > 0 ? ` (${norm.tags.join(", ")})` : "" + lines.push( + `- **${norm.ruleId}**${tagList} — Zero violations since ${norm.extinctSince.toISOString().split("T")[0]}` + ) + lines.push(` - Fixed ${norm.totalFixed} violations over ${norm.zeroSpan.days} days`) + if (norm.docsUrl) { + lines.push(` - Docs: ${norm.docsUrl}`) + } + lines.push(``) + } + } + } + + // Migration threads section + if (threads.length > 0) { + lines.push(`## Migration History`) + lines.push(``) + lines.push(`Key Amp threads that contributed to this directory's migration:`) + lines.push(``) + + for (const thread of threads) { + const dateRange = `${thread.first.toISOString().split("T")[0]} → ${thread.last.toISOString().split("T")[0]}` + const fixes = `${thread.fixesDelta.errors} errors, ${thread.fixesDelta.warnings} warnings` + + lines.push(`- **${thread.threadId}** (${dateRange})`) + lines.push(` - Fixed: ${fixes}`) + lines.push(` - Rules addressed: ${thread.topRules.join(", ")}`) + if (thread.threadUrl) { + lines.push(` - Thread: ${thread.threadUrl}`) + } + lines.push(``) + } + } + + // Verification section + lines.push(`## Verification`) + lines.push(``) + lines.push(`Re-run migration audit to verify compliance:`) + lines.push(``) + lines.push(`\`\`\`bash`) + lines.push(`# Audit current state`) + lines.push(`effect-migrate audit --amp-out .amp/effect-migrate`) + lines.push(``) + lines.push(`# Check checkpoint history`) + lines.push(`effect-migrate checkpoints list`) + lines.push(`\`\`\``) + lines.push(``) + + // Footer + lines.push(`---`) + lines.push(``) + lines.push(`*Generated by effect-migrate from checkpoint analysis*`) + + return lines.join("\n") + } + + return AgentsDocGenerator.of({ + generateForDirectory: (directory, options = {}) => + Effect.gen(function* () { + const { overwrite = false, includeThreads = true } = options + + // Get insights data + const status = yield* insights + .classifyDirectories({ minFiles: 1 }) + .pipe(Effect.map((dirs) => dirs.find((d) => d.path === directory))) + + if (!status) { + return yield* Effect.fail( + new Error(`Directory not found in checkpoint history: ${directory}`) + ) + } + + const norms = yield* insights.directoryNorms(directory) + const threads = includeThreads ? yield* insights.directoryThreads(directory) : [] + + // Render template + const content = renderTemplate(status, norms, threads) + + // Write AGENTS.md + const agentsPath = path.join(directory, "AGENTS.md") + + // Check if exists + const exists = yield* fs.exists(agentsPath) + if (exists && !overwrite) { + return yield* Effect.fail( + new Error(`${agentsPath} already exists. Use --overwrite to replace.`) + ) + } + + yield* fs.writeFileString(agentsPath, content) + + return { + path: agentsPath, + status, + normsCount: norms.length, + threadsCount: threads.length + } + }), + + generateAll: (options = {}) => + Effect.gen(function* () { + const { + target = "migrated", + depth = 2, + minFiles = 3, + stabilityCheckpoints = 3, + stabilityDays = 7, + overwrite = false + } = options + + // Classify all directories + const allDirs = yield* insights.classifyDirectories({ depth, minFiles }) + + // Filter by target + const targetDirs = allDirs.filter((dir) => { + if (target === "all") return true + if (target === "migrated") return dir.status === "migrated" + if (target === "in-progress") return dir.status === "in-progress" + return false + }) + + // Generate for each directory + return yield* Effect.forEach( + targetDirs, + (dir) => this.generateForDirectory(dir.path, { overwrite, includeThreads: true }), + { concurrency: 2 } // Sequential to avoid filesystem race + ) + }) + }) + }) +) +``` + +### CLI Integration + +```typescript +// packages/cli/src/commands/agents.ts +import * as Command from "@effect/cli/Command" +import * as Options from "@effect/cli/Options" +import * as Effect from "effect/Effect" +import * as Console from "effect/Console" +import { AgentsInsights, AgentsDocGenerator } from "@effect-migrate/core" + +const agentsGenerateCommand = Command.make( + "generate", + { + ampOut: Options.directory("amp-out").pipe(Options.withDefault(".amp/effect-migrate")), + checkpointDb: Options.file("checkpoint-db").pipe(Options.optional), + target: Options.choice("target", ["migrated", "in-progress", "all"]).pipe( + Options.withDefault("migrated") + ), + depth: Options.integer("depth").pipe(Options.withDefault(2)), + minFiles: Options.integer("min-files").pipe(Options.withDefault(3)), + stabilityCheckpoints: Options.integer("stability-checkpoints").pipe(Options.withDefault(3)), + stabilityDays: Options.integer("stability-days").pipe(Options.withDefault(7)), + overwrite: Options.boolean("overwrite").pipe(Options.withDefault(false)) + }, + (opts) => + Effect.gen(function* () { + yield* Console.log("🔍 Analyzing checkpoint history...") + + const generator = yield* AgentsDocGenerator + + const docs = yield* generator.generateAll({ + target: opts.target, + depth: opts.depth, + minFiles: opts.minFiles, + stabilityCheckpoints: opts.stabilityCheckpoints, + stabilityDays: opts.stabilityDays, + overwrite: opts.overwrite + }) + + if (docs.length === 0) { + yield* Console.log(`\n❌ No directories matched criteria for ${opts.target}`) + return + } + + yield* Console.log(`\n✅ Generated AGENTS.md for ${docs.length} directories:`) + + for (const doc of docs) { + const statusEmoji = + doc.status.status === "migrated" + ? "✅" + : doc.status.status === "in-progress" + ? "🔄" + : "⏸️" + + yield* Console.log( + ` ${statusEmoji} ${doc.path} (${doc.normsCount} norms, ${doc.threadsCount} threads)` + ) + } + + yield* Console.log( + `\n💡 Tip: Amp will auto-load these AGENTS.md files when working in these directories` + ) + }) +) + +export const agentsCommand = Command.make("agents").pipe( + Command.withSubcommands([agentsGenerateCommand]) +) +``` + +### CLI Usage + +```bash +# Generate AGENTS.md for all migrated directories +effect-migrate agents generate --checkpoint-db .amp/effect-migrate/checkpoints.db + +# Generate for all directories (including in-progress) +effect-migrate agents generate --target all + +# Customize stability thresholds +effect-migrate agents generate --stability-checkpoints 5 --stability-days 14 + +# Overwrite existing AGENTS.md files +effect-migrate agents generate --overwrite + +# Adjust directory depth and minimum file count +effect-migrate agents generate --depth 3 --min-files 5 +``` + +### Output Example + +**Generated `src/services/AGENTS.md`:** + +````markdown +--- +generated: 2025-11-08T14:30:00Z +status: migrated +source: effect-migrate checkpoint analysis +--- + +# AGENTS for src/services + +## Status + +**✅ Migrated** + +- **Clean since:** 2025-11-05 +- **Stability:** 5 consecutive checkpoints +- **Files:** 18 total, 18 clean (100%) +- **Last activity:** 2025-11-05 + +## Enforced Norms + +The following rules are consistently enforced in this directory: + +### Architectural Boundaries + +- **no-node-imports** — Zero violations since 2025-11-05 + - Fixed 12 violations over 8 days + - Docs: https://effect.website/docs/guides/platform + +- **no-import-from-legacy** — Zero violations since 2025-11-04 + - Fixed 8 violations over 10 days + +### Code Patterns + +- **no-async-await** (async, migration) — Zero violations since 2025-11-05 + - Fixed 24 violations over 8 days + - Docs: https://effect.website/docs/essentials/effect-type + +- **no-promise-constructor** (async, error-handling) — Zero violations since 2025-11-04 + - Fixed 15 violations over 10 days + +- **use-effect-console** (logging, services) — Zero violations since 2025-11-03 + - Fixed 18 violations over 12 days + +## Migration History + +Key Amp threads that contributed to this directory's migration: + +- **T-a96b5e77-ba99-4c03-89e9-d7c9145c9700** (2025-11-03 → 2025-11-05) + - Fixed: 18 errors, 32 warnings + - Rules addressed: no-async-await, no-node-imports, use-effect-console + - Thread: https://ampcode.com/threads/T-a96b5e77-ba99-4c03-89e9-d7c9145c9700 + +- **T-5d39c44c-3f5e-4112-b0b1-d9b9add1eea7** (2025-11-04 → 2025-11-05) + - Fixed: 6 errors, 12 warnings + - Rules addressed: no-promise-constructor, no-import-from-legacy + +## Verification + +Re-run migration audit to verify compliance: + +```bash +# Audit current state +effect-migrate audit --amp-out .amp/effect-migrate + +# Check checkpoint history +effect-migrate checkpoints list +``` + +--- + +_Generated by effect-migrate from checkpoint analysis_ +```` + +**Effort:** 6-10 hours + +--- + +## Phase 2: LLM-Enhanced Generation with @effect/ai + +> **💡 NOTE:** If using "context prep + Amp" approach from Phase 1 notes, this phase becomes unnecessary. +> Amp itself provides the LLM capabilities, so we'd just invoke Amp differently. +> +> This phase only makes sense if we're doing the full deterministic implementation. + +**Goal:** Add optional AI-powered summaries for thread impacts and pattern descriptions. + +**Effort:** 4-6 hours + +### Dependencies + +```bash +pnpm add @effect/ai @effect/ai-openai +``` + +### Enhanced AgentsDocGenerator with AI + +```typescript +import { LanguageModel } from "@effect/ai" +import { OpenAiLanguageModel, OpenAiClient } from "@effect/ai-openai" +import * as Config from "effect/Config" +import * as Option from "effect/Option" + +/** + * Optional AI layer for enhanced documentation generation. + */ +export const OptionalAiLayer = Effect.gen(function* () { + const apiKey = yield* Config.redacted("OPENAI_API_KEY").pipe(Config.option) + + if (Option.isNone(apiKey)) { + yield* Console.log("ℹ️ No OPENAI_API_KEY - using deterministic templates only") + return Layer.empty + } + + return Layer.mergeAll( + OpenAiLanguageModel.layer({ model: "gpt-4o-mini" }), + OpenAiClient.layerConfig({ apiKey: apiKey.value }) + ) +}) + +/** + * Generate AI summary of thread impact. + */ +const generateThreadSummary = (thread: ThreadImpact) => + Effect.gen(function* () { + const prompt = ` +Summarize the migration work done in this Amp coding agent thread in 2-3 sentences: + +Thread ID: ${thread.threadId} +Date Range: ${thread.first.toISOString().split("T")[0]} to ${thread.last.toISOString().split("T")[0]} +Fixes: ${thread.fixesDelta.errors} errors, ${thread.fixesDelta.warnings} warnings +Rules Addressed: ${thread.topRules.join(", ")} + +Focus on what patterns were established and architectural changes made. +Be concise and technical. + `.trim() + + const response = yield* LanguageModel.generateText({ prompt }).pipe( + Effect.timeout(Duration.seconds(15)), + Effect.catchAll(() => + Effect.succeed({ + text: `Fixed ${thread.fixesDelta.errors + thread.fixesDelta.warnings} findings across ${thread.topRules.length} rules.` + }) + ) + ) + + return response.text + }) + +/** + * Generate AI summary of directory migration journey. + */ +const generateMigrationSummary = (status: DirectoryStatus, norms: readonly NormEvidence[]) => + Effect.gen(function* () { + const prompt = ` +Summarize the migration journey for this directory in 3-5 sentences: + +Directory: ${status.path} +Status: ${status.status} +Files: ${status.fileCounts.total} total, ${status.fileCounts.clean} clean (${status.fileCounts.cleanPercentage}%) +Clean since: ${status.cleanSince?.toISOString().split("T")[0] ?? "N/A"} + +Key norms established: +${norms.map((n) => `- ${n.ruleId}: Fixed ${n.totalFixed} violations over ${n.zeroSpan.days} days`).join("\n")} + +Focus on the architectural improvements and pattern adoption. +Write for technical audience (developers and AI coding agents). + `.trim() + + const response = yield* LanguageModel.generateText({ prompt }).pipe( + Effect.timeout(Duration.seconds(20)), + Effect.catchAll(() => + Effect.succeed({ + text: `Migrated ${status.fileCounts.total} files by enforcing ${norms.length} rules.` + }) + ) + ) + + return response.text + }) + +/** + * Enhanced template renderer with AI summaries. + */ +const renderTemplateWithAi = ( + status: DirectoryStatus, + norms: readonly NormEvidence[], + threads: readonly ThreadImpact[], + options: { + readonly migrationSummary?: string + readonly threadSummaries?: Map + } +) => { + const lines: string[] = [] + + // ... (same header and status as before) ... + + // AI-generated migration overview (if available) + if (options.migrationSummary) { + lines.push(`## Migration Overview`) + lines.push(``) + lines.push(options.migrationSummary) + lines.push(``) + } + + // ... (same enforced norms as before) ... + + // Enhanced migration threads with AI summaries + if (threads.length > 0) { + lines.push(`## Migration History`) + lines.push(``) + lines.push(`Key Amp threads that contributed to this directory's migration:`) + lines.push(``) + + for (const thread of threads) { + const dateRange = `${thread.first.toISOString().split("T")[0]} → ${thread.last.toISOString().split("T")[0]}` + const summary = options.threadSummaries?.get(thread.threadId) + + lines.push(`### ${thread.threadId}`) + lines.push(``) + lines.push(`**Period:** ${dateRange}`) + + if (summary) { + lines.push(``) + lines.push(summary) + } + + lines.push(``) + lines.push(`**Impact:**`) + lines.push( + `- Fixed ${thread.fixesDelta.errors} errors, ${thread.fixesDelta.warnings} warnings` + ) + lines.push(`- Rules: ${thread.topRules.join(", ")}`) + + if (thread.threadUrl) { + lines.push(`- [View thread](${thread.threadUrl})`) + } + lines.push(``) + } + } + + // ... (same verification and footer) ... + + return lines.join("\n") +} +``` + +### Enhanced CLI with AI Options + +```typescript +const agentsGenerateCommand = Command.make( + "generate", + { + // ... (existing options) ... + llm: Options.choice("llm", ["off", "threads", "full"]).pipe(Options.withDefault("off")) + }, + (opts) => + Effect.gen(function* () { + yield* Console.log("🔍 Analyzing checkpoint history...") + + // Load optional AI layer + const aiLayer = opts.llm !== "off" ? yield* OptionalAiLayer : Layer.empty + + const generator = yield* AgentsDocGenerator + + // Generate with AI enhancement based on --llm flag + const docs = yield* generator + .generateAll({ + target: opts.target, + depth: opts.depth, + minFiles: opts.minFiles, + stabilityCheckpoints: opts.stabilityCheckpoints, + stabilityDays: opts.stabilityDays, + overwrite: opts.overwrite, + aiEnhancement: opts.llm // Pass to generator + }) + .pipe(Effect.provide(aiLayer)) + + // ... (same output as before) ... + }) +) +``` + +### CLI Usage with AI + +```bash +# Generate with AI thread summaries only +effect-migrate agents generate --llm threads + +# Generate with full AI enhancement (migration overview + thread summaries) +effect-migrate agents generate --llm full + +# Requires OPENAI_API_KEY environment variable +export OPENAI_API_KEY=sk-... +effect-migrate agents generate --llm full +``` + +**Effort:** 4-6 hours + +--- + +## Phase 3: Amp SDK Integration for Programmatic Workflows + +> **✨ INSIGHT:** This phase is actually the SIMPLEST approach! +> +> If we start here instead of Phases 1-2, we get: +> - Minimal code (just context prep + Amp invocation) +> - Better quality (Amp writes better docs than templates) +> - Thread awareness (Amp can read-thread automatically) +> - Flexibility (users can tweak prompts) +> +> **Recommendation:** Consider implementing Phase 3 FIRST as MVP, then add deterministic fallback later if needed. + +**Goal:** Enable programmatic invocation of Amp for advanced documentation generation tasks. + +**Effort:** 2-4 hours + +### Use Cases + +1. **Thread Content Analysis**: Read thread content programmatically to extract migration decisions +2. **Automated Documentation**: Generate comprehensive docs by combining checkpoint data with Amp's analysis +3. **Cross-Thread Pattern Detection**: Identify common patterns across multiple migration threads + +### Dependencies + +```bash +pnpm add @sourcegraph/amp +``` + +> **💡 EVEN SIMPLER:** Instead of using CLI with `--execute`, we could: +> +> 1. Import `@sourcegraph/amp` SDK directly +> 2. Wrap SDK methods in Effect services +> 3. Use Effect's resource management for Amp sessions +> +> **Benefits:** +> - Type-safe API instead of CLI string parsing +> - Better error handling (structured errors vs exit codes) +> - Can reuse Amp sessions across multiple operations +> - Composable with other Effect services +> - No subprocess overhead +> +> **Example:** +> ```typescript +> import { AmpClient } from "@sourcegraph/amp" +> +> export const AmpServiceLive = Layer.effect( +> AmpService, +> Effect.gen(function* () { +> const client = yield* Effect.acquireRelease( +> Effect.sync(() => new AmpClient({ apiKey: process.env.AMP_API_KEY })), +> (client) => Effect.sync(() => client.close()) +> ) +> +> return AmpService.of({ +> executePrompt: (prompt) => Effect.promise(() => client.execute(prompt)), +> readThread: (threadId) => Effect.promise(() => client.getThread(threadId)) +> }) +> }) +> ) +> ``` + +### Amp Service Interface + +```typescript +import * as Effect from "effect/Effect" +import * as Context from "effect/Context" +import * as Schema from "effect/Schema" + +/** + * Amp thread analysis result. + */ +export const AmpThreadAnalysis = Schema.Struct({ + threadId: Schema.String, + summary: Schema.String, + keyDecisions: Schema.Array(Schema.String), + patternsAdopted: Schema.Array(Schema.String) +}) + +export type AmpThreadAnalysis = Schema.Schema.Type + +/** + * Amp SDK integration service. + */ +export interface AmpServiceInterface { + /** + * Analyze thread content to extract migration decisions. + */ + readonly analyzeThread: ( + threadId: string, + prompt: string + ) => Effect.Effect + + /** + * Execute Amp task and return result. + */ + readonly executeTask: ( + prompt: string, + context?: readonly string[] + ) => Effect.Effect +} + +export class AmpService extends Context.Tag("AmpService")() {} +``` + +### Implementation + +```typescript +import { Command as NodeCommand } from "@effect/platform-node" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Console from "effect/Console" + +/** + * Amp SDK service implementation. + * + * Uses Amp CLI with --execute and --stream-json for programmatic access. + */ +export const AmpServiceLive = Layer.effect( + AmpService, + Effect.gen(function* () { + const command = yield* NodeCommand.Command + + return AmpService.of({ + analyzeThread: (threadId, prompt) => + Effect.gen(function* () { + yield* Console.log(`📖 Reading thread ${threadId} via Amp...`) + + // Build prompt for thread analysis + const fullPrompt = ` +Read thread ${threadId} using read-thread tool. + +${prompt} + +Return JSON with this exact structure: +{ + "threadId": "${threadId}", + "summary": "2-3 sentence summary", + "keyDecisions": ["decision 1", "decision 2"], + "patternsAdopted": ["pattern 1", "pattern 2"] +} + `.trim() + + // Execute via Amp CLI + const result = yield* command.make("amp", "--execute", fullPrompt, "--stream-json").pipe( + command.exitCode, + Effect.flatMap((code) => + code === 0 + ? Effect.succeed({ + threadId, + summary: "Thread analysis completed", + keyDecisions: [], + patternsAdopted: [] + }) + : Effect.fail(new Error(`Amp execution failed with code ${code}`)) + ) + ) + + return result + }), + + executeTask: (prompt, context = []) => + Effect.gen(function* () { + yield* Console.log(`🤖 Executing Amp task...`) + + // Build context string + const contextStr = + context.length > 0 + ? `\n\nContext files:\n${context.map((f) => `@${f}`).join("\n")}` + : "" + + const fullPrompt = `${prompt}${contextStr}` + + // Execute via Amp CLI + const result = yield* command.make("amp", "--execute", fullPrompt).pipe( + command.lines, + Stream.runCollect, + Effect.map((lines) => Chunk.toArray(lines).join("\n")) + ) + + return result + }) + }) + }) +) +``` + +### Enhanced Generator with Amp Integration + +```typescript +/** + * Enhanced AGENTS.md generation with Amp thread analysis. + */ +const generateWithAmpAnalysis = ( + status: DirectoryStatus, + norms: readonly NormEvidence[], + threads: readonly ThreadImpact[] +) => + Effect.gen(function* () { + const amp = yield* AmpService + + // Analyze each thread with Amp + const threadAnalyses = yield* Effect.forEach( + threads, + (thread) => + amp.analyzeThread( + thread.threadId, + ` +Extract the key migration decisions and patterns adopted in this thread. +Focus on: +- Architectural changes (boundary enforcement, layering) +- Pattern adoption (Effect.gen, services, error handling) +- Rationale for specific approaches + +This thread worked on directory: ${status.path} +Rules addressed: ${thread.topRules.join(", ")} + `.trim() + ), + { concurrency: 2 } // Limit concurrent Amp calls + ) + + // Generate enhanced documentation + const threadSummaries = new Map( + threadAnalyses.map((analysis) => [ + analysis.threadId, + `${analysis.summary}\n\n**Key decisions:** ${analysis.keyDecisions.join(", ")}\n**Patterns:** ${analysis.patternsAdopted.join(", ")}` + ]) + ) + + return renderTemplateWithAi(status, norms, threads, { threadSummaries }) + }) +``` + +### CLI Usage with Amp Integration + +```bash +# Generate with Amp thread analysis +effect-migrate agents generate --llm threads --use-amp + +# Requires AMP_API_KEY environment variable (same as used for Amp sessions) +export AMP_API_KEY=your-api-key +effect-migrate agents generate --llm full --use-amp +``` + +> **🔧 SDK INTEGRATION BENEFITS:** +> +> If we wrap Amp SDK in Effect services, we get: +> +> **Better composability:** +> ```typescript +> // Compose Amp calls with other Effect operations +> const generateDocs = Effect.gen(function* () { +> const summary = yield* prepareSummary() // Effect operation +> const ampResult = yield* AmpService.executePrompt(...) // Amp SDK wrapped +> const written = yield* writeFile(...) // Effect operation +> return written +> }) +> ``` +> +> **Retry/timeout built-in:** +> ```typescript +> const robustGeneration = ampService.executePrompt(prompt).pipe( +> Effect.retry(Schedule.exponential(1000)), +> Effect.timeout(Duration.seconds(30)) +> ) +> ``` +> +> **Testable:** +> ```typescript +> // Mock Amp service for tests +> const AmpServiceMock = Layer.succeed(AmpService, { +> executePrompt: () => Effect.succeed("Generated AGENTS.md content..."), +> readThread: () => Effect.succeed({ decisions: [...] }) +> }) +> ``` +> +> This follows the same pattern we use for FileSystem, SqlClient, etc. + +**Effort:** 2-4 hours + +--- + +## Success Criteria + +### Phase 1 (Deterministic Generation) + +- [ ] AgentsInsights service classifies directories correctly +- [ ] Stability heuristics (K checkpoints, D days) work reliably +- [ ] Norm extraction identifies rules that went to zero +- [ ] Thread impact calculation attributes fixes correctly +- [ ] Template rendering produces valid Markdown +- [ ] CLI command generates AGENTS.md for migrated directories +- [ ] Generated docs include all required sections +- [ ] Performance: <2s to analyze 50 checkpoints, 20 directories + +### Phase 2 (LLM Enhancement) + +- [ ] Optional AI layer loads when OPENAI_API_KEY is set +- [ ] Thread summaries are concise and technical +- [ ] Migration overview captures key improvements +- [ ] Fallback to deterministic templates when AI unavailable +- [ ] Concurrent processing with rate limiting works +- [ ] AI-generated text is contextually relevant + +### Phase 3 (Amp Integration) + +- [ ] Amp CLI execution via --execute works programmatically +- [ ] Thread analysis extracts decisions and patterns +- [ ] Stream JSON parsing handles Amp output +- [ ] Rate limiting prevents excessive Amp API usage +- [ ] Error handling for Amp failures is graceful + +--- + +## Integration Points + +### With Existing Features + +**Checkpoint Persistence (comprehensive-data-architecture Phase 2):** + +- Reads checkpoint history from SQLite or JSON +- Uses manifest.json for efficient traversal +- Leverages normalized schema for memory efficiency + +**Analytics Engine (comprehensive-data-architecture Phase 3):** + +- Extends AnalyticsEngine with directory-scoped queries +- Reuses SQLite aggregations and Polars optimizations +- Shares CheckpointStore abstraction + +**Thread Management (existing):** + +- Reads threads.json for thread associations +- Links checkpoint thread_id to actual thread URLs +- Provides context for AI summarization + +### Data Flow + +``` +Checkpoints (JSON/SQLite) + │ + ▼ +AgentsInsights + ├─► Directory classification + ├─► Norm extraction + └─► Thread impact analysis + │ + ▼ +AgentsDocGenerator + ├─► Template rendering (deterministic) + ├─► Optional: @effect/ai summaries + └─► Optional: Amp SDK analysis + │ + ▼ +Directory-specific AGENTS.md files +``` + +--- + +## File Locations + +### New Files + +``` +packages/core/src/ +├── agents/ +│ ├── AgentsInsights.ts # Service interface + types +│ ├── JsonAgentsInsights.ts # JSON-based implementation +│ ├── SqliteAgentsInsights.ts # SQLite-based implementation +│ ├── AgentsDocGenerator.ts # Template rendering +│ ├── AmpService.ts # Amp SDK integration (Phase 3) +│ └── templates/ +│ └── agents-md.ts # AGENTS.md template + +packages/cli/src/ +├── commands/ +│ └── agents.ts # CLI command implementation +└── formatters/ + └── agents-output.ts # Console output formatting +``` + +> **📦 MINIMAL MVP FILE STRUCTURE:** +> +> If using SDK wrapper approach, you'd only need: +> +> ``` +> packages/core/src/ +> ├── amp/ +> │ └── AmpService.ts # Effect wrapper for @sourcegraph/amp SDK +> └── agents/ +> └── summarizer.ts # Checkpoint → JSON summary (simple) +> +> packages/cli/src/ +> └── commands/ +> └── agents.ts # agents generate command (~100 LOC) +> ``` +> +> **That's it!** Everything else handled by Amp SDK + Effect composition. + +### Modified Files + +``` +packages/core/src/ +└── index.ts # Export new services + +packages/cli/src/ +└── index.ts # Add agents command to CLI +``` + +--- + +## Configuration Options + +### CLI Flags + +```bash +effect-migrate agents generate [options] + +Options: + --amp-out Output directory (default: .amp/effect-migrate) + --checkpoint-db SQLite database path (optional) + --target migrated|in-progress|all (default: migrated) + --depth Directory depth for grouping (default: 2) + --min-files Minimum files per directory (default: 3) + --stability-checkpoints Consecutive zeros required (default: 3) + --stability-days Days clean required (default: 7) + --min-historical-count Min historical violations (default: 3) + --overwrite Overwrite existing AGENTS.md (default: false) + --llm off|threads|full (default: off, requires OPENAI_API_KEY) + --use-amp Use Amp SDK for thread analysis (requires AMP_API_KEY) +``` + +### Environment Variables + +```bash +# For @effect/ai integration +OPENAI_API_KEY=sk-... + +# For Amp SDK integration +AMP_API_KEY=your-api-key +``` + +--- + +## Performance Targets + +| Metric | Target (50 checkpoints, 20 dirs) | Notes | +| ------------------------- | -------------------------------- | ------------------------------ | +| Directory classification | <500ms | SQLite aggregations | +| Norm extraction (per dir) | <200ms | Rule history queries | +| Thread impact (per dir) | <150ms | Checkpoint delta calculations | +| Template rendering | <50ms | Pure string operations | +| Total (deterministic) | <2s | For all qualifying directories | +| AI thread summary | <5s per thread | OpenAI API latency | +| Amp thread analysis | <10s per thread | Amp execution overhead | +| Memory usage | <200MB | Streaming where possible | + +--- + +## Migration Path + +### For Existing Users + +1. **Phase 1 available immediately** after checkpoint persistence is implemented +2. **No breaking changes** to existing checkpoint format +3. **Opt-in feature** - doesn't affect existing workflows +4. **Progressive enhancement** - can start with deterministic, add AI later + +### Adoption Strategy + +```bash +# Step 1: Build checkpoint history (run audit several times over days/weeks) +effect-migrate audit --amp-out .amp/effect-migrate --checkpoint-db checkpoints.db + +# Step 2: Once directories stabilize, generate AGENTS.md +effect-migrate agents generate + +# Step 3: Review generated docs, adjust thresholds if needed +effect-migrate agents generate --stability-checkpoints 5 --overwrite + +# Step 4 (optional): Add AI enhancement +export OPENAI_API_KEY=sk-... +effect-migrate agents generate --llm full --overwrite + +# Step 5: Commit generated AGENTS.md files to repo +git add src/**/AGENTS.md +git commit -m "docs: add auto-generated directory AGENTS.md files" +``` + +--- + +## Risks and Mitigations + +### Risk: False Positives (Temporary Zeros) + +**Problem:** Directory temporarily clean, then regresses. + +**Mitigation:** + +- Require K consecutive checkpoints AND D days clean +- Set reasonable defaults (3 checkpoints, 7 days) +- Allow users to adjust thresholds +- Include "last activity" date in generated docs + +### Risk: Sparse/Irregular Checkpoints + +**Problem:** Users run audits infrequently, not enough data. + +**Mitigation:** + +- Document minimum checkpoint requirements +- Surface warnings when history is insufficient +- Fall back to "provisionally migrated" status +- Suggest running audits more frequently + +### Risk: Directory Renames/Refactors + +**Problem:** Paths change, norms fragment across old/new paths. + +**Mitigation:** + +- Treat paths textually (simple, predictable) +- Future: Add path remap config for major refactors +- Document limitation in generated AGENTS.md header + +### Risk: Performance on Large Histories + +**Problem:** 200+ checkpoints across 50+ directories. + +**Mitigation:** + +- Use SQLite aggregations (efficient) +- Limit analysis to last N checkpoints (default 50) +- Enable Polars for complex trailing-zero calculations +- Process directories with concurrency control + +### Risk: LLM Cost and Privacy + +**Problem:** AI summaries cost money, may leak sensitive info. + +**Mitigation:** + +- Make AI **opt-in** via --llm flag +- Default to deterministic templates (free, private) +- Use gpt-4o-mini (cheap, fast) by default +- Document costs and privacy considerations +- Support self-hosted models via OpenRouter + +--- + +## Future Enhancements + +### Beyond Initial Implementation + +**Rule Taxonomy Registry:** + +- Central mapping of rule IDs to categories +- Better norm grouping and descriptions +- Shared across presets + +**Boundary Verification Graphs:** + +- Visualize import boundaries over time +- ASCII diagrams in AGENTS.md +- Detect circular dependencies + +**Cross-Directory Analysis:** + +- Identify shared patterns across directories +- Suggest preset creation from norms +- Team-wide migration insights + +**VS Code Extension:** + +- Inline AGENTS.md hints +- Quick actions to re-generate docs +- Checkpoint history viewer + +**MCP Server Integration:** + +- Expose insights via MCP protocol +- Enable Amp to query migration status +- Real-time norm verification + +--- + +## Effort Summary + +| Phase | Effort | Deliverable | +| ------------------------ | ------ | ------------------------------------------ | +| Phase 1: Core Generation | 6-10h | Deterministic AGENTS.md from checkpoints | +| Phase 2: AI Enhancement | 4-6h | Optional LLM summaries with @effect/ai | +| Phase 3: Amp Integration | 2-4h | Programmatic thread analysis via Amp SDK | +| **Total** | 12-20h | Production-ready directory docs generation | + +> **⚡ ALTERNATIVE PATH (RECOMMENDED FOR MVP):** +> +> | Approach | Effort | Deliverable | +> | ------------------------------- | ------ | ---------------------------------------- | +> | Context Prep + Amp Invocation | 2-4h | JSON summaries + Amp CLI integration | +> +> **Implementation:** +> +> 1. Build simple checkpoint summarizer (query latest checkpoint, group by directory, extract norms) +> 2. Write directory summaries as JSON +> 3. Invoke `amp --execute` with prompt to generate AGENTS.md +> 4. Done! +> +> **Upgrade path:** If users need standalone operation or deterministic output, add Phases 1-2 later. + +--- + +## References + +### Internal Documentation + +- [Comprehensive Data Architecture](./comprehensive-data-architecture.md) +- [Checkpoint-Based Audit Persistence](./checkpoint-based-audit-persistence.md) +- [Schema Versioning and Normalization](./schema-versioning-and-normalization.md) +- [Amp Integration Concepts](../concepts/amp-integration.md) +- [Root AGENTS.md](../../AGENTS.md) + +### External Resources + +- [@effect/ai Documentation](https://effect.website/docs/packages/ai) +- [Amp Manual](https://ampcode.com/manual) +- [Effect Services Pattern](https://effect.website/docs/guides/context-management/services) +- [SQLite Window Functions](https://www.sqlite.org/windowfunctions.html) +- [nodejs-polars](https://pola-rs.github.io/nodejs-polars/) + +--- + +> **🎯 FINAL RECOMMENDATION:** +> +> **Start simple, iterate based on real usage:** +> +> 1. **MVP (Week 1):** Implement "context prep + Amp invocation" approach +> - Minimal code (~200 LOC) +> - Leverages Amp's strengths +> - Gets feedback on what docs users actually want +> +> 2. **Iteration (Week 2+):** Based on user feedback: +> - If users need offline/deterministic: Add Phase 1 templates +> - If Amp summaries aren't good enough: Add custom prompts or Phase 2 @effect/ai +> - If performance becomes issue: Add caching/optimization +> +> 3. **Future:** Only build the complex infrastructure if truly needed +> +> **Why this makes sense:** +> - effect-migrate already assumes Amp usage (it's built FOR Amp) +> - Users already have AMP_API_KEY configured +> - Amp is BETTER at writing documentation than we'd be with templates +> - We can always add deterministic fallback later +> - **Amp SDK in Effect = perfect dogfooding:** We'd be using Effect patterns to wrap Amp SDK, just like we wrap FileSystem, SqlClient, etc. +> +> **Additional benefits of SDK wrapper:** +> - Can reuse Amp's `read-thread` capability (already implemented in SDK) +> - Amp SDK handles authentication, retries, rate limiting +> - Type-safe API surface (no CLI string parsing) +> - Testable with Effect's Layer mocking +> - Composable with our existing Effect services +> +> **Alternative view:** If you want effect-migrate to work standalone (without Amp), then the full Phase 1-3 architecture makes sense. + +--- + +**Last Updated:** 2025-11-08 +**Maintainer:** Ari Dyckovsky +**Status:** Ready for implementation (recommend MVP approach first) +**Next Steps:** Decide on MVP vs full implementation, validate approach with team, prototype context summarizer