{
  "name": "Obsidian \u2192 Aragora \u2192 Linear Pipeline",
  "description": "Complete workflow: Watch Obsidian vault for #ready tagged notes, run multi-agent debate via Aragora, create Linear issues from decisions, write receipts back to Obsidian.",
  "version": "1.0.0",
  "nodes": [
    {
      "id": "1",
      "name": "Obsidian Vault Watcher",
      "type": "n8n-nodes-base.localFileTrigger",
      "position": [
        100,
        300
      ],
      "parameters": {
        "path": "={{ $env.OBSIDIAN_VAULT_PATH }}",
        "events": [
          "change"
        ],
        "options": {
          "recursive": true,
          "ignored": "**/.obsidian/**,**/templates/**"
        }
      },
      "notes": "Watches the Obsidian vault for file changes. Configure OBSIDIAN_VAULT_PATH environment variable."
    },
    {
      "id": "2",
      "name": "Filter Ready Notes",
      "type": "n8n-nodes-base.filter",
      "position": [
        300,
        300
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.path }}",
              "operation": "endsWith",
              "value2": ".md"
            }
          ]
        }
      }
    },
    {
      "id": "3",
      "name": "Read Note Content",
      "type": "n8n-nodes-base.readBinaryFiles",
      "position": [
        500,
        300
      ],
      "parameters": {
        "fileSelector": "={{ $json.path }}"
      }
    },
    {
      "id": "4",
      "name": "Parse Frontmatter",
      "type": "n8n-nodes-base.code",
      "position": [
        700,
        300
      ],
      "parameters": {
        "jsCode": "// Parse YAML frontmatter from markdown\nconst content = $input.first().binary.data.toString('utf-8');\n\nlet frontmatter = {};\nlet body = content;\n\nif (content.startsWith('---')) {\n  const parts = content.split('---');\n  if (parts.length >= 3) {\n    const yaml = parts[1].trim();\n    body = parts.slice(2).join('---').trim();\n    \n    // Simple YAML parsing\n    yaml.split('\\n').forEach(line => {\n      const [key, ...valueParts] = line.split(':');\n      if (key && valueParts.length) {\n        const value = valueParts.join(':').trim();\n        frontmatter[key.trim()] = value.replace(/^[\"']|[\"']$/g, '');\n      }\n    });\n  }\n}\n\n// Check for #ready tag\nconst hasReadyTag = body.includes('#ready') || \n                   (frontmatter.tags && frontmatter.tags.includes('ready'));\n\nreturn {\n  json: {\n    path: $input.first().json.path,\n    filename: $input.first().json.path.split('/').pop(),\n    frontmatter,\n    body,\n    hasReadyTag,\n    title: frontmatter.title || $input.first().json.path.split('/').pop().replace('.md', '')\n  }\n};"
      }
    },
    {
      "id": "5",
      "name": "Check Ready Tag",
      "type": "n8n-nodes-base.if",
      "position": [
        900,
        300
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.hasReadyTag }}",
              "value2": true
            }
          ]
        }
      }
    },
    {
      "id": "6",
      "name": "Launch Aragora Debate",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1100,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $env.ARAGORA_API_URL }}/api/v2/debates",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer {{ $env.ARAGORA_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "task",
              "value": "={{ $json.title }}: {{ $json.body.substring(0, 500) }}"
            },
            {
              "name": "agents",
              "value": "[\"claude\", \"gpt-4\", \"gemini\"]"
            },
            {
              "name": "rounds",
              "value": "3"
            },
            {
              "name": "evidence_sources",
              "value": "[\"knowledge_mound\"]"
            },
            {
              "name": "metadata",
              "value": "={{ JSON.stringify({ obsidian_path: $json.path, obsidian_title: $json.title }) }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "notes": "Launches multi-agent debate via Aragora API. Configure ARAGORA_API_URL and ARAGORA_API_KEY."
    },
    {
      "id": "7",
      "name": "Wait for Debate Completion",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1300,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $env.ARAGORA_API_URL }}/api/v1/debates/{{ $json.debate_id }}/decision-integrity",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer {{ $env.ARAGORA_API_KEY }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "retry": {
            "maxRetries": 10,
            "retryInterval": 5000,
            "retryOnStatus": [
              202
            ]
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"include_receipt\": true,\n  \"include_plan\": true,\n  \"include_context\": false,\n  \"plan_strategy\": \"single_task\",\n  \"notify_origin\": false\n}"
      },
      "notes": "Builds a decision integrity package. Retries until available."
    },
    {
      "id": "8",
      "name": "Create Linear Issue",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1500,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.linear.app/graphql",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "{{ $env.LINEAR_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"query\": \"mutation CreateIssue($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier url title } } }\",\n  \"variables\": {\n    \"input\": {\n      \"teamId\": \"{{ $env.LINEAR_TEAM_ID }}\",\n      \"title\": \"[Decision] {{ $node['Parse Frontmatter'].json.title }}\",\n      \"description\": \"## Decision Summary\n\n{{ $json.receipt?.verdict_reasoning || $json.receipt?.input_summary || 'Decision reached' }}\n\n### Consensus\n- **Reached**: {{ $json.receipt?.consensus_proof?.reached ? 'Yes' : 'No' }}\n- **Confidence**: {{ Math.round(($json.receipt?.consensus_proof?.confidence ?? $json.receipt?.confidence ?? 0) * 100) }}%\n\n### Source\n- Obsidian Note: `{{ $node['Parse Frontmatter'].json.path }}`\n- Debate ID: `{{ $json.debate_id || $json.receipt?.gauntlet_id || 'unknown' }}`\n\n---\n*Generated by Aragora Decision Engine*\",\n      \"priority\": {{ ($json.receipt?.consensus_proof?.confidence ?? $json.receipt?.confidence ?? 0) > 0.8 ? 2 : 3 }},\n      \"labelIds\": [\"{{ $env.LINEAR_DECISION_LABEL_ID }}\"]\n    }\n  }\n}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "notes": "Creates Linear issue from debate decision. Configure LINEAR_API_KEY, LINEAR_TEAM_ID, LINEAR_DECISION_LABEL_ID."
    },
    {
      "id": "9",
      "name": "Write Receipt to Obsidian",
      "type": "n8n-nodes-base.code",
      "position": [
        1700,
        200
      ],
      "parameters": {
        "jsCode": "const fs = require('fs');\nconst path = require('path');\n\nconst packageData = $input.first().json || {};\nconst receipt = packageData.receipt || {};\nconst plan = packageData.plan || {};\nconst originalNote = $node['Parse Frontmatter'].json;\nconst linearIssue = $node['Create Linear Issue'].json.data?.issueCreate?.issue;\n\nconst debateId = packageData.debate_id || receipt.gauntlet_id || receipt.debate_id || 'unknown';\nconst consensusReached = (receipt.consensus_proof && receipt.consensus_proof.reached !== undefined)\n  ? receipt.consensus_proof.reached\n  : ['PASS', 'CONDITIONAL'].includes(String(receipt.verdict || '').toUpperCase());\nconst confidence = (receipt.consensus_proof && receipt.consensus_proof.confidence !== undefined)\n  ? receipt.consensus_proof.confidence\n  : (receipt.confidence || 0);\nconst decisionSummary = receipt.verdict_reasoning || receipt.input_summary || 'Decision reached through multi-agent deliberation.';\nconst planTasks = Array.isArray(plan.tasks) ? plan.tasks : [];\n\nconst planSection = planTasks.length > 0\n  ? `## Implementation Plan\n\n${planTasks.map(t => `- [ ] ${t.id || ''}: ${t.description || ''}${t.complexity ? ` (${t.complexity})` : ''}${t.files && t.files.length ? ` \u2014 Files: ${t.files.join(', ')}` : ''}`).join('\\n')}`\n  : '## Implementation Plan\n\n_No implementation tasks generated_';\n\nconst receiptContent = `---\ntitle: \"Decision Integrity - ${originalNote.title}\"\ndate: ${new Date().toISOString().split('T')[0]}\ntags:\n  - decision\n  - aragora\n  - integrity\naragora_id: ${debateId}\ndebate_id: ${debateId}\nconsensus: ${consensusReached}\nconfidence: ${confidence}\nreceipt_id: ${receipt.receipt_id || 'N/A'}\nplan_task_count: ${planTasks.length}\nlinear_issue: ${linearIssue?.identifier || 'N/A'}\nrelated_notes:\n  - \"[[${originalNote.filename.replace('.md', '')}]]\"\n---\n\n# Decision Integrity: ${originalNote.title}\n\n## Summary\n\n${decisionSummary}\n\n## Outcome\n\n| Metric | Value |\n|--------|-------|\n| Consensus Reached | ${consensusReached ? 'Yes \u2713' : 'No \u2717'} |\n| Confidence Score | ${Math.round(confidence * 100)}% |\n| Participating Agents | ${receipt.consensus_proof?.supporting_agents?.length || 0} |\n| Debate Rounds | ${receipt.probes_run || 0} |\n\n## Linear Issue\n\n${linearIssue ? `[${linearIssue.identifier}](${linearIssue.url}) - ${linearIssue.title}` : 'No issue created'}\n\n## Dissent Trail\n\n${receipt.dissenting_views?.length > 0\n  ? receipt.dissenting_views.map((d, i) => `${i + 1}. ${d}`).join('\\n')\n  : '_No dissent recorded_'}\n\n${planSection}\n\n## Verification\n\n- **Debate ID**: \\`${debateId}\\`\n- **Receipt ID**: \\`${receipt.receipt_id || 'N/A'}\\`\n- **Signature**: \\`${receipt.signature?.substring(0, 20) || 'N/A'}...\\`\n- **Timestamp**: ${receipt.timestamp || new Date().toISOString()}\n\n## Original Note\n\n![[${originalNote.filename.replace('.md', '')}]]\n\n---\n\n*This decision integrity package was automatically generated by Aragora.*\n*Verify at: ${process.env.ARAGORA_API_URL}/verify/${debateId}*\n`;\n\nconst vaultPath = process.env.OBSIDIAN_VAULT_PATH;\nconst decisionsFolder = path.join(vaultPath, 'decisions');\nconst safeTitle = originalNote.title.replace(/[^a-zA-Z0-9\\s-]/g, '').substring(0, 50);\nconst receiptFilename = `${new Date().toISOString().split('T')[0]}-${safeTitle.replace(/\\s+/g, '-')}-integrity.md`;\nconst receiptPath = path.join(decisionsFolder, receiptFilename);\n\nif (!fs.existsSync(decisionsFolder)) {\n  fs.mkdirSync(decisionsFolder, { recursive: true });\n}\n\nfs.writeFileSync(receiptPath, receiptContent, 'utf-8');\n\nconst originalPath = originalNote.path;\nlet originalContent = fs.readFileSync(originalPath, 'utf-8');\noriginalContent = originalContent.replace(/#ready/g, '#processed');\n\nif (originalContent.startsWith('---')) {\n  const parts = originalContent.split('---');\n  if (parts.length >= 3) {\n    let fm = parts[1];\n    if (!fm.includes('aragora_id:')) {\n      fm += `\\naragora_id: ${debateId}`;\n      fm += `\\nlinear_issue: ${linearIssue?.identifier || 'N/A'}`;\n      fm += `\\ndecision_integrity: true`;\n    }\n    originalContent = `---${fm}---${parts.slice(2).join('---')}`;\n  }\n}\n\nfs.writeFileSync(originalPath, originalContent, 'utf-8');\n\nreturn {\n  json: {\n    success: true,\n    receiptPath,\n    originalPath: originalNote.path,\n    debateId,\n    linearIssue: linearIssue?.identifier,\n    linearUrl: linearIssue?.url,\n  }\n};"
      },
      "notes": "Writes decision receipt to Obsidian vault and updates original note."
    },
    {
      "id": "10",
      "name": "Send Slack Notification",
      "type": "n8n-nodes-base.slack",
      "position": [
        1900,
        200
      ],
      "parameters": {
        "operation": "post",
        "channel": "={{ $env.SLACK_CHANNEL }}",
        "text": "\ud83c\udfaf *Decision Made*\n\n*Topic:* {{ $node['Parse Frontmatter'].json.title }}\n*Consensus:* {{ $node['Wait for Debate Completion'].json.receipt?.consensus_proof?.reached ? '\u2713 Reached' : '\u2717 Not reached' }}\n*Confidence:* {{ Math.round(($node['Wait for Debate Completion'].json.receipt?.consensus_proof?.confidence ?? $node['Wait for Debate Completion'].json.receipt?.confidence ?? 0) * 100) }}%\n\n\ud83d\udccb Linear Issue: {{ $node['Create Linear Issue'].json.data?.issueCreate?.issue?.url || 'N/A' }}\n\ud83d\udcdd Receipt: `decisions/{{ $json.receiptPath.split('/').pop() }}`",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "notes": "Optional: Send notification to Slack. Configure SLACK_CHANNEL and Slack credentials."
    }
  ],
  "connections": {
    "Obsidian Vault Watcher": {
      "main": [
        [
          {
            "node": "Filter Ready Notes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Ready Notes": {
      "main": [
        [
          {
            "node": "Read Note Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Note Content": {
      "main": [
        [
          {
            "node": "Parse Frontmatter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Frontmatter": {
      "main": [
        [
          {
            "node": "Check Ready Tag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Ready Tag": {
      "main": [
        [
          {
            "node": "Launch Aragora Debate",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Launch Aragora Debate": {
      "main": [
        [
          {
            "node": "Wait for Debate Completion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Debate Completion": {
      "main": [
        [
          {
            "node": "Create Linear Issue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Linear Issue": {
      "main": [
        [
          {
            "node": "Write Receipt to Obsidian",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Receipt to Obsidian": {
      "main": [
        [
          {
            "node": "Send Slack Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [
    "obsidian",
    "aragora",
    "linear",
    "decision-making",
    "automation"
  ],
  "versionId": "1.0.0"
}