[{"data":1,"prerenderedAt":4769},["Reactive",2],{"certifications":3,"home-beyond":247,"home-stack":285,"home-work":310,"home-experience":571,"home-projects":630,"home-writing":721},[4,22,32,42,53,64,74,85,94,102,112,122,133,144,155,166,177,188,201,213,225,236],{"_path":5,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":9,"date_achieved":10,"valid_until":11,"certification_authority":12,"public_url":13,"img":14,"slot":15,"shape":16,"_id":17,"_type":18,"_source":19,"_file":20,"_stem":21,"_extension":18},"/certifications/aws-cloud-practioner","certifications",false,"","AWS Certified Cloud Practitioner","20/03/2024","27/10/2027","Amazon Web Services","https://www.credly.com/badges/208b6629-a2bf-4ffa-84d2-781c1a1cd649/public_url","certifications/aws-cloud-practioner.png",13,"hex","content:certifications:1.aws-cloud-practioner.json","json","content","certifications/1.aws-cloud-practioner.json","certifications/1.aws-cloud-practioner",{"_path":23,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":24,"date_achieved":25,"valid_until":11,"certification_authority":12,"public_url":26,"img":27,"slot":28,"shape":16,"_id":29,"_type":18,"_source":19,"_file":30,"_stem":31,"_extension":18},"/certifications/aws-ai-practioner","AWS Certified AI Practitioner","27/10/2024","https://www.credly.com/badges/eeb79ba2-0f2a-439f-9b96-658f12a2c8ec/public_url","certifications/aws-ai-practioner.png",14,"content:certifications:2.aws-ai-practioner.json","certifications/2.aws-ai-practioner.json","certifications/2.aws-ai-practioner",{"_path":33,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":34,"date_achieved":35,"valid_until":36,"certification_authority":12,"public_url":37,"img":38,"_id":39,"_type":18,"_source":19,"_file":40,"_stem":41,"_extension":18},"/certifications/aws-ai-practioner-early-adopter","AWS Certified AI Practitioner Early Adopter","12/07/2024","N/A","https://www.credly.com/badges/84bb48e5-bd99-427d-acaf-0b0c099b8eaa/public_url","certifications/aws-ai-practioner-early-adopter.png","content:certifications:3.aws-ai-practioner-early-adopter.json","certifications/3.aws-ai-practioner-early-adopter.json","certifications/3.aws-ai-practioner-early-adopter",{"_path":43,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":44,"date_achieved":45,"valid_until":46,"certification_authority":12,"public_url":47,"img":48,"slot":49,"shape":16,"_id":50,"_type":18,"_source":19,"_file":51,"_stem":52,"_extension":18},"/certifications/aws-solutions-architect-associate","AWS Certified Solutions Architect – Associate","03/03/2022","21/07/2028","https://www.credly.com/badges/10049954-e5d2-4c19-b738-e414e663278f/public_url","certifications/aws-solutions-architect-associate.png",8,"content:certifications:4.aws-solutions-architect-associate.json","certifications/4.aws-solutions-architect-associate.json","certifications/4.aws-solutions-architect-associate",{"_path":54,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":55,"date_achieved":56,"valid_until":57,"certification_authority":12,"public_url":58,"img":59,"slot":60,"shape":16,"_id":61,"_type":18,"_source":19,"_file":62,"_stem":63,"_extension":18},"/certifications/aws-sysops-associate","AWS Certified SysOps Administrator – Associate","10/04/2024","01/06/2027","https://www.credly.com/badges/51927533-169e-4c88-b748-f6ebdc687b9c/public_url","certifications/aws-sysops-associate.png",10,"content:certifications:5.aws-sysops-associate.json","certifications/5.aws-sysops-associate.json","certifications/5.aws-sysops-associate",{"_path":65,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":66,"date_achieved":67,"valid_until":57,"certification_authority":12,"public_url":68,"img":69,"slot":70,"shape":16,"_id":71,"_type":18,"_source":19,"_file":72,"_stem":73,"_extension":18},"/certifications/aws-developer-associate","AWS Certified Developer – Associate","29/05/2024","https://www.credly.com/badges/fa4815fd-e951-4bda-bfc3-3cc27755d8e4/public_url","certifications/aws-developer-associate.png",9,"content:certifications:6.aws-developer-associate.json","certifications/6.aws-developer-associate.json","certifications/6.aws-developer-associate",{"_path":75,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":76,"date_achieved":77,"valid_until":78,"certification_authority":12,"public_url":79,"img":80,"slot":81,"shape":16,"_id":82,"_type":18,"_source":19,"_file":83,"_stem":84,"_extension":18},"/certifications/aws-data-associate","AWS Certified Data Engineer – Associate","17/04/2024","17/04/2027","https://www.credly.com/badges/457dfa80-f9aa-4f60-afde-3a8f28d0e8f2/public_url","certifications/aws-data-associate.png",11,"content:certifications:7.aws-data-associate.json","certifications/7.aws-data-associate.json","certifications/7.aws-data-associate",{"_path":86,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":87,"date_achieved":25,"valid_until":11,"certification_authority":12,"public_url":88,"img":89,"slot":90,"shape":16,"_id":91,"_type":18,"_source":19,"_file":92,"_stem":93,"_extension":18},"/certifications/aws-ml-associate","AWS Certified Machine Learning Engineer – Associate","https://www.credly.com/badges/9947cfdb-a8ec-42cf-aa30-e3e6fc1e2be8/public_url","certifications/aws-ml-associate.png",12,"content:certifications:8.aws-ml-associate.json","certifications/8.aws-ml-associate.json","certifications/8.aws-ml-associate",{"_path":95,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":96,"date_achieved":25,"valid_until":36,"certification_authority":12,"public_url":97,"img":98,"_id":99,"_type":18,"_source":19,"_file":100,"_stem":101,"_extension":18},"/certifications/aws-ml-associate-early-adopter","AWS Certified Machine Learning Engineer - Associate Early Adopter","https://www.credly.com/badges/c1258cf2-25cb-4b71-abb2-5aabd0b88f5a/public_url","certifications/aws-ml-associate-early-adopter.png","content:certifications:9.aws-ml-associate-early-adopter.json","certifications/9.aws-ml-associate-early-adopter.json","certifications/9.aws-ml-associate-early-adopter",{"_path":103,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":104,"date_achieved":105,"valid_until":57,"certification_authority":12,"public_url":106,"img":107,"slot":108,"shape":16,"_id":109,"_type":18,"_source":19,"_file":110,"_stem":111,"_extension":18},"/certifications/aws-devops-professional","AWS Certified DevOps Engineer – Professional","01/06/2024","https://www.credly.com/badges/e1d33372-17fc-4393-99c6-8e43e28c979e/public_url","certifications/aws-devops-professional.png",1,"content:certifications:10.aws-devops-professional.json","certifications/10.aws-devops-professional.json","certifications/10.aws-devops-professional",{"_path":113,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":114,"date_achieved":115,"valid_until":46,"certification_authority":12,"public_url":116,"img":117,"slot":118,"shape":16,"_id":119,"_type":18,"_source":19,"_file":120,"_stem":121,"_extension":18},"/certifications/aws-solutions-architect-professional","AWS Certified Solutions Architect – Professional","21/07/2022","https://www.credly.com/badges/a8efa210-4571-4d5c-819e-7f29f13e6d0a/public_url","certifications/aws-solutions-architect-professional.png",0,"content:certifications:11.aws-solutions-architect-professional.json","certifications/11.aws-solutions-architect-professional.json","certifications/11.aws-solutions-architect-professional",{"_path":123,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":124,"date_achieved":125,"valid_until":126,"certification_authority":12,"public_url":127,"img":128,"slot":129,"shape":16,"_id":130,"_type":18,"_source":19,"_file":131,"_stem":132,"_extension":18},"/certifications/aws-security-speciality","AWS Certified Security – Specialty","24/09/2023","31/08/2029","https://www.credly.com/badges/8468e841-7654-4b49-9e62-595089230830/public_url","certifications/aws-security-specialty.png",2,"content:certifications:12.aws-security-speciality.json","certifications/12.aws-security-speciality.json","certifications/12.aws-security-speciality",{"_path":134,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":135,"date_achieved":136,"valid_until":137,"certification_authority":12,"public_url":138,"img":139,"slot":140,"shape":16,"_id":141,"_type":18,"_source":19,"_file":142,"_stem":143,"_extension":18},"/certifications/aws-ml-speciality","AWS Certified Machine Learning – Specialty","27/01/2024","27/01/2027","https://www.credly.com/badges/ebfb3a7c-094e-47e4-a4fb-79d096160aeb/public_url","certifications/aws-ml-specialty.png",5,"content:certifications:13.aws-ml-speciality.json","certifications/13.aws-ml-speciality.json","certifications/13.aws-ml-speciality",{"_path":145,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":146,"date_achieved":147,"valid_until":148,"certification_authority":12,"public_url":149,"img":150,"slot":151,"shape":16,"_id":152,"_type":18,"_source":19,"_file":153,"_stem":154,"_extension":18},"/certifications/aws-networking-speciality","AWS Certified Advanced Networking – Specialty","20/06/2024","20/06/2027","https://www.credly.com/badges/1514be88-56fe-4a21-a230-3aa92fc2f336/public_url","certifications/aws-networking-specialty.png",3,"content:certifications:14.aws-networking-speciality.json","certifications/14.aws-networking-speciality.json","certifications/14.aws-networking-speciality",{"_path":156,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":157,"date_achieved":158,"valid_until":159,"certification_authority":12,"public_url":160,"img":161,"slot":162,"shape":16,"_id":163,"_type":18,"_source":19,"_file":164,"_stem":165,"_extension":18},"/certifications/aws-data-specialty","AWS Certified Data Analytics – Specialty","08/01/2024","08/01/2027","https://www.credly.com/badges/b6409379-4607-4592-bc51-68a9cf2724b3/public_url","certifications/aws-data-specialty.png",6,"content:certifications:15.aws-data-specialty.json","certifications/15.aws-data-specialty.json","certifications/15.aws-data-specialty",{"_path":167,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":168,"date_achieved":169,"valid_until":170,"certification_authority":12,"public_url":171,"img":172,"slot":173,"shape":16,"_id":174,"_type":18,"_source":19,"_file":175,"_stem":176,"_extension":18},"/certifications/aws-database-specialty","AWS Certified Database – Specialty","17/11/2023","17/11/2026","https://www.credly.com/badges/5b220898-1020-471b-a478-a412e1122447/public_url","certifications/aws-database-specialty.png",4,"content:certifications:16.aws-database-specialty.json","certifications/16.aws-database-specialty.json","certifications/16.aws-database-specialty",{"_path":178,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":179,"date_achieved":180,"valid_until":181,"certification_authority":12,"public_url":182,"img":183,"slot":184,"shape":16,"_id":185,"_type":18,"_source":19,"_file":186,"_stem":187,"_extension":18},"/certifications/aws-sap-specialty","AWS Certified: SAP on AWS – Specialty","14/03/2024","14/03/2027","https://www.credly.com/badges/5916d02b-cfb2-45a9-a65d-79cfa23a2152/public_url","certifications/aws-sap-specialty.png",7,"content:certifications:17.aws-sap-specialty.json","certifications/17.aws-sap-specialty.json","certifications/17.aws-sap-specialty",{"_path":189,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":190,"date_achieved":191,"valid_until":192,"certification_authority":193,"public_url":194,"img":195,"slot":196,"shape":197,"_id":198,"_type":18,"_source":19,"_file":199,"_stem":200,"_extension":18},"/certifications/terraform-associate","HashiCorp Certified: Terraform Associate (003)","29/09/2023","29/09/2027","HashiCorp","https://www.credly.com/badges/5c448180-a825-4dc8-91c7-4ce68d1be78d/public_url","certifications/terraform-associate.png",17,"round","content:certifications:18.terraform-associate.json","certifications/18.terraform-associate.json","certifications/18.terraform-associate",{"_path":202,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":203,"date_achieved":204,"valid_until":205,"certification_authority":206,"public_url":207,"img":208,"slot":209,"shape":197,"_id":210,"_type":18,"_source":19,"_file":211,"_stem":212,"_extension":18},"/certifications/github-actions","GitHub Actions","08/07/2024","08/07/2027","Github","https://www.credly.com/badges/6bfad13d-2391-4315-a8cd-0cef78f16f36/public_url","certifications/github-actions.png",18,"content:certifications:19.github-actions.json","certifications/19.github-actions.json","certifications/19.github-actions",{"_path":214,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":215,"date_achieved":216,"valid_until":217,"certification_authority":218,"public_url":219,"img":220,"slot":221,"shape":197,"_id":222,"_type":18,"_source":19,"_file":223,"_stem":224,"_extension":18},"/certifications/azure-admin-associate","Microsoft Certified: Azure Administrator Associate","01/10/2022","01/10/2027","Microsoft","https://learn.microsoft.com/api/credentials/share/en-us/MarcoEndrizzi-7807/CF32CA4B111C8910?sharingId=75A200E9426E5366","certifications/azure-admin-associate.png",15,"content:certifications:20.azure-admin-associate.json","certifications/20.azure-admin-associate.json","certifications/20.azure-admin-associate",{"_path":226,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":227,"date_achieved":228,"valid_until":229,"certification_authority":218,"public_url":230,"img":231,"slot":232,"shape":197,"_id":233,"_type":18,"_source":19,"_file":234,"_stem":235,"_extension":18},"/certifications/azure-dev-associate","Microsoft Certified: Azure Developer Associate","10/09/2023","11/09/2025","https://learn.microsoft.com/api/credentials/share/en-us/MarcoEndrizzi-7807/AB8F21689BD0952E?sharingId=75A200E9426E5366","certifications/azure-dev-associate.png",16,"content:certifications:21.azure-dev-associate.json","certifications/21.azure-dev-associate.json","certifications/21.azure-dev-associate",{"_path":237,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":238,"date_achieved":239,"valid_until":240,"certification_authority":12,"public_url":241,"img":242,"slot":243,"shape":16,"_id":244,"_type":18,"_source":19,"_file":245,"_stem":246,"_extension":18},"/certifications/aws-cloudops-associate","AWS Certified CloudOps Engineer – Associate","20/10/2025","20/10/2028","https://www.credly.com/badges/455a6f2b-a33b-401c-ae9e-fc0db04a39fa/public_url","certifications/aws-cloudops-associate.png",19,"content:certifications:22.aws-cloudops-associate.json","certifications/22.aws-cloudops-associate.json","certifications/22.aws-cloudops-associate",{"_path":248,"_dir":249,"_draft":7,"_partial":7,"_locale":8,"groups":250,"_id":281,"_type":18,"title":282,"_source":19,"_file":283,"_stem":284,"_extension":18},"/site/beyond","site",[251,266],{"title":252,"items":253},"Recognition",[254,257,260,263],{"title":255,"detail":256},"Reply Group Certification Challenge","Winner, 2025",{"title":258,"detail":259},"Lightning Bolt Award, twice","Storm Reply's quarterly award for its best-performing employee",{"title":261,"detail":262},"Fivium Prize","University of Surrey, 2023",{"title":264,"detail":265},"Sky Star Award","Sky, 2022",{"title":267,"items":268},"People",[269,272,275,278],{"title":270,"detail":271},"Lead technical interviewer","Designed the graduate hiring process and question bank, 2025 and 2026 cohorts",{"title":273,"detail":274},"Line manager","For a graduate consultant",{"title":276,"detail":277},"Product Guild lead","Storm UK's internal tooling group",{"title":279,"detail":280},"Stormcademy co-organiser","Frontend and backend training, plus the certification pathway guides","content:site:beyond.json","Beyond","site/beyond.json","site/beyond",{"_path":286,"_dir":249,"_draft":7,"_partial":7,"_locale":8,"rows":287,"_id":306,"_type":18,"title":307,"_source":19,"_file":308,"_stem":309,"_extension":18},"/site/stack",[288,291,294,297,300,303],{"label":289,"value":290},"Cloud","AWS (Lambda, ECS, EKS, VPC, IAM, DynamoDB, EventBridge, Bedrock AgentCore), Azure",{"label":292,"value":293},"Infrastructure","Terraform, Kubernetes, Docker, Helm, Ansible",{"label":295,"value":296},"Delivery","GitHub Actions, GitLab CI/CD, Jenkins",{"label":298,"value":299},"Monitoring","CloudWatch, Grafana, Sensu",{"label":301,"value":302},"AI engineering","Claude Code, Codex, Kiro, multi-agent orchestration and cost tracking",{"label":304,"value":305},"Languages","Python, TypeScript, SQL, Rust, React, Vue","content:site:stack.json","Stack","site/stack.json","site/stack",[311,445,490,531],{"_path":312,"_dir":313,"_draft":7,"_partial":7,"_locale":8,"title":314,"description":8,"kicker":315,"summary":316,"cover":317,"coverSmall":318,"coverAlt":319,"role":320,"where":321,"stack":322,"usedBy":326,"stats":327,"next":334,"order":108,"body":335,"_type":440,"_id":441,"_source":19,"_file":442,"_stem":443,"_extension":444},"/work/software-toolkit","work","Software Toolkit","Internal product · Storm Reply","A Terraform scaffold that provisions serverless infrastructure, mocks APIs locally with AWS SAM and gives every developer their own sandbox per branch.","/3d/cover-software-toolkit.webp","/3d/cover-software-toolkit-640.webp","Clay model of a stack of slabs with a teal branch leading to a smaller sandbox slab","Built it, lead the guild that owns it","Storm Reply UK",[323,324,325,203],"Terraform","AWS SAM","Python","20+ engineers",[328,331],{"value":329,"label":330},"20+","engineers using it",{"value":332,"label":333},"2 weeks → hours","new project setup","agentic-delivery-platform",{"type":336,"children":337,"toc":438},"root",[338,360,424],{"type":339,"tag":340,"props":341,"children":343},"element","case-section",{"label":342},"The problem",[344,351],{"type":339,"tag":345,"props":346,"children":347},"p",{},[348],{"type":349,"value":350},"text","Setting up a new project used to take about two weeks before anyone wrote a feature: infrastructure, pipelines, environments and a way to test the API locally.",{"type":339,"tag":352,"props":353,"children":354},"case-placeholder",{},[355],{"type":339,"tag":345,"props":356,"children":357},{},[358],{"type":349,"value":359},"[In your words: what that setup looked like before the toolkit, and what went wrong when developers tested infrastructure changes.]",{"type":339,"tag":340,"props":361,"children":364},{"label":362,"gap":363},"How it works","steps",[365,376,386,414],{"type":339,"tag":366,"props":367,"children":370},"case-step",{"number":368,"title":369},"1","Scaffold the serverless basics",[371],{"type":339,"tag":345,"props":372,"children":373},{},[374],{"type":349,"value":375},"Twelve Terraform modules cover the building blocks of a serverless app, and a wizard scaffolds each new API endpoint so every project starts from the same shape.",{"type":339,"tag":366,"props":377,"children":380},{"number":378,"title":379},"2","Run the API locally",[381],{"type":339,"tag":345,"props":382,"children":383},{},[384],{"type":349,"value":385},"AWS SAM runs the API on your machine against the same Terraform code that deploys it.",{"type":339,"tag":366,"props":387,"children":391},{"number":388,"title":389,"accent":390},"3","A sandbox for every branch","true",[392,406],{"type":339,"tag":345,"props":393,"children":394},{},[395,397,404],{"type":349,"value":396},"The branch name is hashed into its own Terraform state, so anyone can run ",{"type":339,"tag":398,"props":399,"children":401},"code",{"className":400},[],[402],{"type":349,"value":403},"terraform apply",{"type":349,"value":405}," from their laptop without touching anyone else's stack.",{"type":339,"tag":407,"props":408,"children":409},"template",{"v-slot:extra":8},[410],{"type":339,"tag":411,"props":412,"children":413},"branch-diagram",{},[],{"type":339,"tag":366,"props":415,"children":418},{"number":416,"title":417},"4","Keep it tidy",[419],{"type":339,"tag":345,"props":420,"children":421},{},[422],{"type":349,"value":423},"aws-nuke clears out abandoned sandboxes, and Checkov, tflint and pre-commit hooks run on every change.",{"type":339,"tag":340,"props":425,"children":427},{"label":426},"Results",[428,433],{"type":339,"tag":429,"props":430,"children":432},"case-stats",{":items":431},"[{\"value\":\"2 weeks → hours\",\"label\":\"to set up a new project\"},{\"value\":\"20+\",\"label\":\"engineers use it\"},{\"value\":\"Most used\",\"label\":\"internal tool at Storm Reply UK\"}]",[],{"type":339,"tag":345,"props":434,"children":435},{},[436],{"type":349,"value":437},"It's also what the company's internal Promotion App, for goals and feedback, was built on.",{"title":8,"searchDepth":129,"depth":129,"links":439},[],"markdown","content:work:software-toolkit.md","work/software-toolkit.md","work/software-toolkit","md",{"_path":446,"_dir":313,"_draft":7,"_partial":7,"_locale":8,"title":447,"description":8,"kicker":448,"summary":449,"cover":450,"coverSmall":451,"coverAlt":452,"role":453,"where":454,"stack":455,"stats":459,"next":466,"order":129,"body":467,"_type":440,"_id":487,"_source":19,"_file":488,"_stem":489,"_extension":444},"/work/agentic-delivery-platform","Agentic delivery platform","Lead engineer · global travel company","Moving a Jira ticket to Agent Ready hands it to agents on Amazon Bedrock AgentCore, which take it through requirements, architecture, implementation and code review.","/3d/cover-agentic-platform.webp","/3d/cover-agentic-platform-640.webp","Clay model of a conveyor with four upright cards travelling along it, one of the tiles on the rail picked out in teal","Lead engineer","Global travel company",[456,457,458,325],"Amazon Bedrock AgentCore","Jira","GitLab",[460,463],{"value":461,"label":462},"3 agents","Claude Code, Codex, Kiro, interchangeable",{"value":464,"label":465},"Per agent","token and cost tracking","alarm-framework",{"type":336,"children":468,"toc":485},[469],{"type":339,"tag":340,"props":470,"children":472},{"label":471},"Overview",[473,477],{"type":339,"tag":345,"props":474,"children":475},{},[476],{"type":349,"value":449},{"type":339,"tag":352,"props":478,"children":479},{},[480],{"type":339,"tag":345,"props":481,"children":482},{},[483],{"type":349,"value":484},"[In your words: how the platform is put together, what the handover between the agents looks like, and what changed for the team once tickets started coming back as pull requests.]",{"title":8,"searchDepth":129,"depth":129,"links":486},[],"content:work:agentic-delivery-platform.md","work/agentic-delivery-platform.md","work/agentic-delivery-platform",{"_path":491,"_dir":313,"_draft":7,"_partial":7,"_locale":8,"title":492,"description":8,"kicker":493,"summary":494,"cover":495,"coverSmall":496,"coverAlt":497,"where":498,"stack":499,"stats":501,"next":508,"order":151,"body":509,"_type":440,"_id":528,"_source":19,"_file":529,"_stem":530,"_extension":444},"/work/alarm-framework","Alarms for a whole AWS organisation","Monitoring · UK national lottery operator","A Python data source finds resources across every account at plan time, and Terraform generates their CloudWatch alarms from per-service specs. A daily GitHub Actions run keeps it current.","/3d/cover-alarm-framework.webp","/3d/cover-alarm-framework-640.webp","Clay model of a grid of small blocks with pin lights on top, two of them lit teal and one dark","UK national lottery operator",[323,325,500,203],"CloudWatch",[502,505],{"value":503,"label":504},"109","alarm modules",{"value":506,"label":507},"Every account","compute, databases, networking","terraform-guard-rails",{"type":336,"children":510,"toc":526},[511],{"type":339,"tag":340,"props":512,"children":513},{"label":471},[514,518],{"type":339,"tag":345,"props":515,"children":516},{},[517],{"type":349,"value":494},{"type":339,"tag":352,"props":519,"children":520},{},[521],{"type":339,"tag":345,"props":522,"children":523},{},[524],{"type":349,"value":525},"[In your words: which services the specs cover, how the daily run reconciles alarms with the resources that came and went, and who picks the alerts up.]",{"title":8,"searchDepth":129,"depth":129,"links":527},[],"content:work:alarm-framework.md","work/alarm-framework.md","work/alarm-framework",{"_path":532,"_dir":313,"_draft":7,"_partial":7,"_locale":8,"title":533,"description":8,"kicker":534,"summary":535,"cover":536,"coverSmall":537,"coverAlt":538,"role":539,"where":321,"stack":540,"stats":541,"next":548,"order":173,"body":549,"_type":440,"_id":568,"_source":19,"_file":569,"_stem":570,"_extension":444},"/work/terraform-guard-rails","Terraform with guard rails","Company template · Storm Reply","The starting point for new AWS projects: agent skills for Terraform work, a plan review that flags IAM widening, replacements and cost changes, and tested hooks that block destructive changes.","/3d/cover-terraform-template.webp","/3d/cover-terraform-template-640.webp","Clay model of a dark disc on a plinth, fenced off behind a teal gate","Wrote it",[323,203],[542,545],{"value":543,"label":544},"22","Claude Code and Codex skills",{"value":546,"label":547},"Every PR","plans all environments, gated apply","software-toolkit",{"type":336,"children":550,"toc":566},[551],{"type":339,"tag":340,"props":552,"children":553},{"label":471},[554,558],{"type":339,"tag":345,"props":555,"children":556},{},[557],{"type":349,"value":535},{"type":339,"tag":352,"props":559,"children":560},{},[561],{"type":339,"tag":345,"props":562,"children":563},{},[564],{"type":349,"value":565},"[In your words: what the plan review catches that a human reviewer misses, how the hooks are tested, and how the template has changed since the first project used it.]",{"title":8,"searchDepth":129,"depth":129,"links":567},[],"content:work:terraform-guard-rails.md","work/terraform-guard-rails.md","work/terraform-guard-rails",[572,602,617],{"_path":573,"_dir":574,"_draft":7,"_partial":7,"_locale":8,"order":108,"org":321,"location":575,"start":576,"end":577,"marker":578,"role":579,"roles":580,"bullets":597,"_id":598,"_type":18,"title":599,"_source":19,"_file":600,"_stem":601,"_extension":18},"/experience/storm-reply","experience","London","2023",null,"office","Senior Cloud Consultant",[581,588],{"role":579,"start":582,"end":577,"bullets":583},"Mar 2025",[584,585,586,587],"Lead engineer on an agentic product-development platform for a global travel company, including the layer that makes Claude Code, Codex and Kiro interchangeable.","Architected hybrid networking for a global glass manufacturer and set up agentic development on their repository, delivering 10+ full-stack screens to a tight deadline.","Lead the Product Guild, which owns the Software Toolkit, and wrote the company's Terraform template for new AWS projects.","Lead technical interviewer for graduate hiring, line manager to a graduate consultant, and org admin for AWS, GitHub, Atlassian, Figma and Slack.",{"role":589,"note":590,"start":591,"end":582,"bullets":592},"Cloud Consultant I → III","(skip-level promotion)","Sep 2023",[593,594,595,596],"Requested by a UK national lottery operator to join their monitoring and alerting team: organisation-wide CloudWatch alarms, Grafana dashboards and Sensu checks still in daily use.","Owned their Amazon Connect and ServiceNow contact-centre platform: infrastructure, security and platform code.","Reworked their Terraform GitHub Actions workflows and wrote the reusable Docker pipeline with Trivy scanning used across their GitHub Enterprise.","Closed 55% more tickets than the team average in 2024.",[],"content:experience:1.storm-reply.json","Storm Reply","experience/1.storm-reply.json","experience/1.storm-reply",{"_path":603,"_dir":574,"_draft":7,"_partial":7,"_locale":8,"order":129,"org":604,"location":605,"start":606,"end":607,"marker":608,"role":609,"bullets":610,"_id":614,"_type":18,"title":604,"_source":19,"_file":615,"_stem":616,"_extension":18},"/experience/sky","Sky","Osterley","Jul 2021","Jul 2022","dish","Associate DevOps Engineer, placement year",[611,612,613],"Designed and built ALPI, a serverless log-processing platform across AWS and Azure, used department-wide.","Designed and led the migration of the team's first application to AWS.","Refactored the team's GitLab CI/CD pipelines into shared templates reused by every repository.","content:experience:2.sky.json","experience/2.sky.json","experience/2.sky",{"_path":618,"_dir":574,"_draft":7,"_partial":7,"_locale":8,"order":151,"org":619,"location":620,"start":621,"end":576,"marker":622,"role":623,"summary":624,"bullets":625,"_id":626,"_type":18,"title":627,"_source":19,"_file":628,"_stem":629,"_extension":18},"/experience/university-of-surrey","University of Surrey","Guildford","2019","cap","BSc Computer Science, First Class Honours","Final mark 86, with five courseworks at 100. Dissertation on private digital transactions in Rust.",[],"content:experience:3.university-of-surrey.json","University Of Surrey","experience/3.university-of-surrey.json","experience/3.university-of-surrey",[631,651,671,685,697,709],{"_path":632,"_dir":633,"_draft":7,"_partial":7,"_locale":8,"name":634,"url":635,"description":636,"summary":637,"details":638,"thumbnail":639,"logo":639,"status":640,"featured":641,"live":641,"stats":642,"collaborators":646,"opensource":7,"_id":647,"_type":18,"title":648,"_source":19,"_file":649,"_stem":650,"_extension":18},"/projects/candlealpha","projects","CandleAlpha","https://candlealpha.com","Browser game that trains you to read candlestick charts","A game that trains you to read candlestick charts. Built alone in 10 days of development through an agentic loop, with Codex reviewing Claude Code working from Linear tickets.","CandleAlpha shows you an anonymised futures chart and asks you to predict the next move. Guess right and your streak grows, guess wrong and the game explains what the chart was telling you. Daily challenges are scored on a shared leaderboard. Built with React, TypeScript, Tailwind and Zustand on top of klinecharts, with market data pulled from Yahoo Finance and deployed on Cloudflare Pages with a D1 database for challenge scores.","/projects/candlealpha-logo.png","Live",true,[643,644,645],"33k lines of TypeScript","1,100+ tests","Cloudflare D1",[],"content:projects:1.candlealpha.json","Candlealpha","projects/1.candlealpha.json","projects/1.candlealpha",{"_path":652,"_dir":633,"_draft":7,"_partial":7,"_locale":8,"name":653,"url":654,"description":655,"summary":656,"details":657,"thumbnail":658,"cover":659,"coverAlt":660,"status":661,"featured":641,"live":7,"stats":662,"collaborators":666,"opensource":641,"_id":667,"_type":18,"title":668,"_source":19,"_file":669,"_stem":670,"_extension":18},"/projects/netnotes","NetNotes","https://github.com/endrizzimarco/dissertation-2023/blob/master/main.pdf","A protocol for fully private transactions, with the fastest known implementation of its proofs","A protocol for fully private transactions built on Mimblewimble and One-out-of-Many proofs, and an optimised Rust implementation of those proofs, the fastest in any language.","Final year dissertation at the University of Surrey. NetNotes designs a protocol for fully private digital transactions on top of Mimblewimble and One-out-of-Many proofs, and benchmarks it against existing privacy coins. The work also optimised the only public Rust implementation of One-out-of-Many proofs: proofs 65% smaller, generation 96% faster and verification 65% faster.","/3d/cover-netnotes-640.webp","/3d/cover-netnotes.webp","Clay model of a sealed envelope resting on a stack of paper notes","Dissertation · Rust",[663,664,665],"96% faster proving","65% smaller proofs","Paper and video",[],"content:projects:2.netnotes.json","Netnotes","projects/2.netnotes.json","projects/2.netnotes",{"_path":672,"_dir":633,"_draft":7,"_partial":7,"_locale":8,"name":673,"url":674,"description":675,"details":676,"thumbnail":677,"status":678,"tag":679,"featured":7,"collaborators":680,"opensource":641,"_id":681,"_type":18,"title":682,"_source":19,"_file":683,"_stem":684,"_extension":18},"/projects/pocoin","POCoin","https://pocoin.co.uk/","A cryptocurrency with Paxos consensus across replicated Elixir nodes","POCoin is a blockchain visualization tool that allows you to inspect how a cryptocurrency works. The cryptocurrency was built using Elixir and exposed via a simple Cowboy webserver API. The frontend was built using Vue.js and Ant Design. It was developed as part of a university project in Distributed Systems.","/projects/pocoin-logo.png","Proof of Concept","Best coursework prize",[],"content:projects:3.pocoin.json","Pocoin","projects/3.pocoin.json","projects/3.pocoin",{"_path":686,"_dir":633,"_draft":7,"_partial":7,"_locale":8,"name":687,"url":688,"description":689,"details":690,"thumbnail":8,"status":691,"tag":261,"featured":7,"collaborators":692,"opensource":7,"_id":693,"_type":18,"title":694,"_source":19,"_file":695,"_stem":696,"_extension":18},"/projects/freshfarm","FreshFarm","https://www.youtube.com/watch?v=doKs4dPQhlU","A farmers' marketplace on Azure Functions with a Vue frontend","A marketplace connecting local farmers with buyers, built on Azure Functions in C#, JavaScript and Python with a Vue and Quasar frontend. Coursework for the Advanced Web Technologies module at the University of Surrey, awarded the Fivium Prize for the highest mark on the module.","University project",[],"content:projects:4.freshfarm.json","Freshfarm","projects/4.freshfarm.json","projects/4.freshfarm",{"_path":698,"_dir":633,"_draft":7,"_partial":7,"_locale":8,"name":699,"url":700,"description":701,"details":702,"thumbnail":8,"status":640,"tag":703,"featured":7,"collaborators":704,"opensource":7,"_id":705,"_type":18,"title":706,"_source":19,"_file":707,"_stem":708,"_extension":18},"/projects/aido-bz","AIDO Bolzano","https://aido.bz.it","Website for the Bolzano branch of the Italian organ donation association","A static site for AIDO Bolzano covering how organ donation works, volunteer testimonials and the association's book. Built with Astro and deployed on Cloudflare, with a focus on fast loading and accessibility for an older audience.","Astro · Cloudflare",[],"content:projects:5.aido-bz.json","Aido Bz","projects/5.aido-bz.json","projects/5.aido-bz",{"_path":710,"_dir":633,"_draft":7,"_partial":7,"_locale":8,"name":711,"url":712,"description":713,"details":714,"thumbnail":8,"status":691,"tag":715,"featured":7,"collaborators":716,"opensource":641,"_id":717,"_type":18,"title":718,"_source":19,"_file":719,"_stem":720,"_extension":18},"/projects/nightlyfe","NightLyfe","https://github.com/endrizzimarco/NightLyfe","A real-time nightlife map for groups, on Firebase","A mobile web app that shows where a group of friends is on a live map of a city's nightlife, with venue queues, ratings and meeting points. Built in Vue and Quasar on Firebase as a university group project.","Vue · Quasar",[],"content:projects:6.nightlyfe.json","Nightlyfe","projects/6.nightlyfe.json","projects/6.nightlyfe",[722,2203,3821],{"_path":723,"_dir":724,"_draft":7,"_partial":7,"_locale":8,"title":725,"description":726,"published":727,"slug":728,"body":729,"_type":440,"_id":2200,"_source":19,"_file":2201,"_stem":2202,"_extension":444},"/articles/how-i-let-ai-run-my-life","articles","How I let AI run my life (and what it isn't allowed to see)","I keep my finances, plans, health and journal in a markdown wiki that Claude maintains. Here is how it is built, and the four rules that made it safe enough to trust.","2026/9/8","how-i-let-ai-run-my-life",{"type":336,"children":730,"toc":2191},[731,744,749,756,780,789,794,800,814,819,967,972,978,992,1000,1013,1054,1059,1064,1169,1198,1204,1209,1659,1664,1700,1730,1735,1744,1750,1764,2017,2039,2044,2050,2063,2169,2175,2180,2185],{"type":339,"tag":345,"props":732,"children":733},{},[734,736,742],{"type":349,"value":735},"Since July I have kept a folder called ",{"type":339,"tag":398,"props":737,"children":739},{"className":738},[],[740],{"type":349,"value":741},"~/life",{"type":349,"value":743},". It holds my bank transactions, my ten-year financial plan, a page for every person I care about, my step counts and sleep, a journal, and a backlog of things I keep meaning to do around the house. Most of it is maintained by Claude Code. Some of it is maintained by a small model that never leaves my laptop. The journal is maintained by nobody but me.",{"type":339,"tag":345,"props":745,"children":746},{},[747],{"type":349,"value":748},"The title is a bit of a lie. The AI doesn't run my life, it does the filing. But the filing is what I was bad at, and this post is about how to build the same thing without handing your bank statements and your diary to a model provider. Four rules did most of the work.",{"type":339,"tag":750,"props":751,"children":753},"h2",{"id":752},"the-pattern",[754],{"type":349,"value":755},"The pattern",{"type":339,"tag":345,"props":757,"children":758},{},[759,761,770,772,778],{"type":349,"value":760},"The starting point is Andrej Karpathy's ",{"type":339,"tag":762,"props":763,"children":767},"a",{"href":764,"rel":765},"https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f",[766],"nofollow",[768],{"type":349,"value":769},"LLM Wiki",{"type":349,"value":771},". Raw sources go in one folder and are never edited. A model derives a wiki of markdown pages from them. A schema file, ",{"type":339,"tag":398,"props":773,"children":775},{"className":774},[],[776],{"type":349,"value":777},"CLAUDE.md",{"type":349,"value":779},", tells the model how the wiki is organised and what the rules are. Three operations: ingest a source, query the wiki, lint it.",{"type":339,"tag":345,"props":781,"children":782},{},[783],{"type":339,"tag":784,"props":785,"children":788},"img",{"alt":786,"src":787},"Architecture of the life wiki: immutable sources on the left, a Python pipeline in the middle, the markdown wiki and its readers on the right","/articles/life-wiki-architecture.svg",[],{"type":339,"tag":345,"props":790,"children":791},{},[792],{"type":349,"value":793},"I changed two things. Pages are organised by domain rather than by concept, because a research wiki accumulates and a life wiki overwrites. My mortgage page is the current state of one mortgage, not an essay. And there is a SQLite sidecar, which is rule one.",{"type":339,"tag":750,"props":795,"children":797},{"id":796},"rule-1-numbers-in-sqlite-words-in-markdown",[798],{"type":349,"value":799},"Rule 1: numbers in SQLite, words in markdown",{"type":339,"tag":345,"props":801,"children":802},{},[803,805,812],{"type":349,"value":804},"A year of bank transactions cannot live in markdown. It floods the context of any model that reads it and it is useless to a human anyway. So the rule is strict in both directions. Never put a transaction row in a page. Never put a sentence in a database. ",{"type":339,"tag":762,"props":806,"children":809},{"href":807,"rel":808},"https://obsidian.md",[766],[810],{"type":349,"value":811},"Obsidian",{"type":349,"value":813}," cannot render SQLite, so a script writes a monthly summary page from the database. The database is the truth. The summary is what gets read.",{"type":339,"tag":345,"props":815,"children":816},{},[817],{"type":349,"value":818},"The payoff is that every money question goes through SQL, and you can encode the mistakes you have already made into a view. This is the only thing the monthly finance review is allowed to query:",{"type":339,"tag":820,"props":821,"children":825},"pre",{"className":822,"code":823,"language":824,"meta":8,"style":8},"language-sql shiki shiki-themes github-dark","-- Real spending only. Every \"how much did I spend\" question starts here, never\n-- from the transactions table.\n--\n-- is_internal matters more than it looks: funding the joint account from the\n-- personal one is money leaving personal AND later leaving joint, so counting\n-- both double-counts it. Monzo marks most of those include_in_spending=1, so\n-- its own flag is not sufficient.\nCREATE VIEW IF NOT EXISTS spending AS\nSELECT t.*, m.name AS merchant_name, a.is_joint\nFROM transactions t\nLEFT JOIN merchants m ON m.id = t.merchant_id\nJOIN accounts a       ON a.id = t.account_id\nWHERE t.declined = 0\n  AND t.include_in_spending = 1\n  AND t.is_internal = 0\n  AND t.is_load = 0\n  AND t.amount_pence \u003C 0;\n","sql",[826],{"type":339,"tag":398,"props":827,"children":828},{"__ignoreMap":8},[829,839,847,855,863,871,879,887,895,903,911,919,927,935,943,951,959],{"type":339,"tag":830,"props":831,"children":833},"span",{"class":832,"line":108},"line",[834],{"type":339,"tag":830,"props":835,"children":836},{},[837],{"type":349,"value":838},"-- Real spending only. Every \"how much did I spend\" question starts here, never\n",{"type":339,"tag":830,"props":840,"children":841},{"class":832,"line":129},[842],{"type":339,"tag":830,"props":843,"children":844},{},[845],{"type":349,"value":846},"-- from the transactions table.\n",{"type":339,"tag":830,"props":848,"children":849},{"class":832,"line":151},[850],{"type":339,"tag":830,"props":851,"children":852},{},[853],{"type":349,"value":854},"--\n",{"type":339,"tag":830,"props":856,"children":857},{"class":832,"line":173},[858],{"type":339,"tag":830,"props":859,"children":860},{},[861],{"type":349,"value":862},"-- is_internal matters more than it looks: funding the joint account from the\n",{"type":339,"tag":830,"props":864,"children":865},{"class":832,"line":140},[866],{"type":339,"tag":830,"props":867,"children":868},{},[869],{"type":349,"value":870},"-- personal one is money leaving personal AND later leaving joint, so counting\n",{"type":339,"tag":830,"props":872,"children":873},{"class":832,"line":162},[874],{"type":339,"tag":830,"props":875,"children":876},{},[877],{"type":349,"value":878},"-- both double-counts it. Monzo marks most of those include_in_spending=1, so\n",{"type":339,"tag":830,"props":880,"children":881},{"class":832,"line":184},[882],{"type":339,"tag":830,"props":883,"children":884},{},[885],{"type":349,"value":886},"-- its own flag is not sufficient.\n",{"type":339,"tag":830,"props":888,"children":889},{"class":832,"line":49},[890],{"type":339,"tag":830,"props":891,"children":892},{},[893],{"type":349,"value":894},"CREATE VIEW IF NOT EXISTS spending AS\n",{"type":339,"tag":830,"props":896,"children":897},{"class":832,"line":70},[898],{"type":339,"tag":830,"props":899,"children":900},{},[901],{"type":349,"value":902},"SELECT t.*, m.name AS merchant_name, a.is_joint\n",{"type":339,"tag":830,"props":904,"children":905},{"class":832,"line":60},[906],{"type":339,"tag":830,"props":907,"children":908},{},[909],{"type":349,"value":910},"FROM transactions t\n",{"type":339,"tag":830,"props":912,"children":913},{"class":832,"line":81},[914],{"type":339,"tag":830,"props":915,"children":916},{},[917],{"type":349,"value":918},"LEFT JOIN merchants m ON m.id = t.merchant_id\n",{"type":339,"tag":830,"props":920,"children":921},{"class":832,"line":90},[922],{"type":339,"tag":830,"props":923,"children":924},{},[925],{"type":349,"value":926},"JOIN accounts a       ON a.id = t.account_id\n",{"type":339,"tag":830,"props":928,"children":929},{"class":832,"line":15},[930],{"type":339,"tag":830,"props":931,"children":932},{},[933],{"type":349,"value":934},"WHERE t.declined = 0\n",{"type":339,"tag":830,"props":936,"children":937},{"class":832,"line":28},[938],{"type":339,"tag":830,"props":939,"children":940},{},[941],{"type":349,"value":942},"  AND t.include_in_spending = 1\n",{"type":339,"tag":830,"props":944,"children":945},{"class":832,"line":221},[946],{"type":339,"tag":830,"props":947,"children":948},{},[949],{"type":349,"value":950},"  AND t.is_internal = 0\n",{"type":339,"tag":830,"props":952,"children":953},{"class":832,"line":232},[954],{"type":339,"tag":830,"props":955,"children":956},{},[957],{"type":349,"value":958},"  AND t.is_load = 0\n",{"type":339,"tag":830,"props":960,"children":961},{"class":832,"line":196},[962],{"type":339,"tag":830,"props":963,"children":964},{},[965],{"type":349,"value":966},"  AND t.amount_pence \u003C 0;\n",{"type":339,"tag":345,"props":968,"children":969},{},[970],{"type":349,"value":971},"Money is signed integer pence. Every parser deletes its database and rebuilds it from the raw JSON on every run, so a parser bug is fixed by editing the parser, never by re-fetching. On Sunday mornings a job rebuilds each database into a scratch file and compares row counts with the live one, to prove the databases are still fully derived from the sources.",{"type":339,"tag":750,"props":973,"children":975},{"id":974},"rule-2-plumbing-not-agents",[976],{"type":349,"value":977},"Rule 2: plumbing, not agents",{"type":339,"tag":345,"props":979,"children":980},{},[981,983,990],{"type":349,"value":982},"The part I am most pleased with has no AI in it. Nine ",{"type":339,"tag":762,"props":984,"children":987},{"href":985,"rel":986},"https://keith.github.io/xcode-man-pages/launchd.plist.5.html",[766],[988],{"type":349,"value":989},"launchd",{"type":349,"value":991}," agents run on a schedule. Monzo at 07:30, Google Health at 07:45, Habitica at 07:50, a lint at 08:10, Trading 212 at 18:10 after the London close, a commit at 23:45. They are Python, they read JSON, they write rows. If something breaks it breaks in a log I can read.",{"type":339,"tag":345,"props":993,"children":994},{},[995],{"type":339,"tag":784,"props":996,"children":999},{"alt":997,"src":998},"Output of launchctl list filtered to life-wiki, showing nine loaded jobs","/articles/life-wiki-launchctl.png",[],{"type":339,"tag":345,"props":1001,"children":1002},{},[1003,1005,1011],{"type":349,"value":1004},"Two launchd tricks are worth stealing. The first is ",{"type":339,"tag":398,"props":1006,"children":1008},{"className":1007},[],[1009],{"type":349,"value":1010},"WatchPaths",{"type":349,"value":1012}," instead of a timer. The nightly distillation writes proposals into an inbox file as checkboxes. Ticking one in Obsidian saves the file, launchd notices, and a script pushes the ticked lines to Habitica. No terminal, no waiting for a window.",{"type":339,"tag":820,"props":1014,"children":1018},{"className":1015,"code":1016,"language":1017,"meta":8,"style":8},"language-xml shiki shiki-themes github-dark","\u003Ckey>WatchPaths\u003C/key>\n\u003Carray>\n  \u003Cstring>/Users/marcoendrizzi/life/inbox/distil-proposals.md\u003C/string>\n\u003C/array>\n","xml",[1019],{"type":339,"tag":398,"props":1020,"children":1021},{"__ignoreMap":8},[1022,1030,1038,1046],{"type":339,"tag":830,"props":1023,"children":1024},{"class":832,"line":108},[1025],{"type":339,"tag":830,"props":1026,"children":1027},{},[1028],{"type":349,"value":1029},"\u003Ckey>WatchPaths\u003C/key>\n",{"type":339,"tag":830,"props":1031,"children":1032},{"class":832,"line":129},[1033],{"type":339,"tag":830,"props":1034,"children":1035},{},[1036],{"type":349,"value":1037},"\u003Carray>\n",{"type":339,"tag":830,"props":1039,"children":1040},{"class":832,"line":151},[1041],{"type":339,"tag":830,"props":1042,"children":1043},{},[1044],{"type":349,"value":1045},"  \u003Cstring>/Users/marcoendrizzi/life/inbox/distil-proposals.md\u003C/string>\n",{"type":339,"tag":830,"props":1047,"children":1048},{"class":832,"line":173},[1049],{"type":339,"tag":830,"props":1050,"children":1051},{},[1052],{"type":349,"value":1053},"\u003C/array>\n",{"type":339,"tag":345,"props":1055,"children":1056},{},[1057],{"type":349,"value":1058},"The script rewrites the watched file after a push, which re-triggers the agent, but the second run finds every line marked and exits. A lock file stops two rapid autosaves from posting the same line twice.",{"type":339,"tag":345,"props":1060,"children":1061},{},[1062],{"type":349,"value":1063},"The second trick exists because a Trading 212 snapshot once sat unrefreshed for thirteen days before I noticed. Every scheduled script now stamps a JSON file on its success path only, written temp-and-rename so a crash cannot corrupt the other stamps. The lint compares the stamps against each job's expected cadence and flags anything stale:",{"type":339,"tag":820,"props":1065,"children":1069},{"className":1066,"code":1067,"language":1068,"meta":8,"style":8},"language-python shiki shiki-themes github-dark","JOB_CADENCE = {\n    \"commit\": \"1d\",                 # 23:45\n    \"distil\": \"1d\",                 # 03:00\n    \"google_health_sync\": \"1d\",     # 07:45\n    \"habitica_push\": None,          # WatchPaths on inbox/distil-proposals.md\n    \"habitica_sync\": \"1d\",          # 07:50\n    \"lint\": \"1d\",                   # 08:10\n    \"monzo_sync\": \"1d\",             # 07:30\n    \"promote\": None,                # WatchPaths on inbox/distil-proposals.md\n    \"rebuild\": \"1w\",                # Sundays 09:00\n    \"trading212_sync\": \"1d\",        # 18:10\n}\n","python",[1070],{"type":339,"tag":398,"props":1071,"children":1072},{"__ignoreMap":8},[1073,1081,1089,1097,1105,1113,1121,1129,1137,1145,1153,1161],{"type":339,"tag":830,"props":1074,"children":1075},{"class":832,"line":108},[1076],{"type":339,"tag":830,"props":1077,"children":1078},{},[1079],{"type":349,"value":1080},"JOB_CADENCE = {\n",{"type":339,"tag":830,"props":1082,"children":1083},{"class":832,"line":129},[1084],{"type":339,"tag":830,"props":1085,"children":1086},{},[1087],{"type":349,"value":1088},"    \"commit\": \"1d\",                 # 23:45\n",{"type":339,"tag":830,"props":1090,"children":1091},{"class":832,"line":151},[1092],{"type":339,"tag":830,"props":1093,"children":1094},{},[1095],{"type":349,"value":1096},"    \"distil\": \"1d\",                 # 03:00\n",{"type":339,"tag":830,"props":1098,"children":1099},{"class":832,"line":173},[1100],{"type":339,"tag":830,"props":1101,"children":1102},{},[1103],{"type":349,"value":1104},"    \"google_health_sync\": \"1d\",     # 07:45\n",{"type":339,"tag":830,"props":1106,"children":1107},{"class":832,"line":140},[1108],{"type":339,"tag":830,"props":1109,"children":1110},{},[1111],{"type":349,"value":1112},"    \"habitica_push\": None,          # WatchPaths on inbox/distil-proposals.md\n",{"type":339,"tag":830,"props":1114,"children":1115},{"class":832,"line":162},[1116],{"type":339,"tag":830,"props":1117,"children":1118},{},[1119],{"type":349,"value":1120},"    \"habitica_sync\": \"1d\",          # 07:50\n",{"type":339,"tag":830,"props":1122,"children":1123},{"class":832,"line":184},[1124],{"type":339,"tag":830,"props":1125,"children":1126},{},[1127],{"type":349,"value":1128},"    \"lint\": \"1d\",                   # 08:10\n",{"type":339,"tag":830,"props":1130,"children":1131},{"class":832,"line":49},[1132],{"type":339,"tag":830,"props":1133,"children":1134},{},[1135],{"type":349,"value":1136},"    \"monzo_sync\": \"1d\",             # 07:30\n",{"type":339,"tag":830,"props":1138,"children":1139},{"class":832,"line":70},[1140],{"type":339,"tag":830,"props":1141,"children":1142},{},[1143],{"type":349,"value":1144},"    \"promote\": None,                # WatchPaths on inbox/distil-proposals.md\n",{"type":339,"tag":830,"props":1146,"children":1147},{"class":832,"line":60},[1148],{"type":339,"tag":830,"props":1149,"children":1150},{},[1151],{"type":349,"value":1152},"    \"rebuild\": \"1w\",                # Sundays 09:00\n",{"type":339,"tag":830,"props":1154,"children":1155},{"class":832,"line":81},[1156],{"type":339,"tag":830,"props":1157,"children":1158},{},[1159],{"type":349,"value":1160},"    \"trading212_sync\": \"1d\",        # 18:10\n",{"type":339,"tag":830,"props":1162,"children":1163},{"class":832,"line":90},[1164],{"type":339,"tag":830,"props":1165,"children":1166},{},[1167],{"type":349,"value":1168},"}\n",{"type":339,"tag":345,"props":1170,"children":1171},{},[1172,1174,1180,1182,1188,1190,1196],{"type":349,"value":1173},"A job with ",{"type":339,"tag":398,"props":1175,"children":1177},{"className":1176},[],[1178],{"type":349,"value":1179},"None",{"type":349,"value":1181}," is exempt, because a quiet week there just means nothing was ticked. Claude Code sits on top of all this. It reads the schema, the pages and the databases, answers questions with citations, and files new sources into the right pages. A ",{"type":339,"tag":398,"props":1183,"children":1185},{"className":1184},[],[1186],{"type":349,"value":1187},"PostToolUse",{"type":349,"value":1189}," hook appends every edit to ",{"type":339,"tag":398,"props":1191,"children":1193},{"className":1192},[],[1194],{"type":349,"value":1195},"log.md",{"type":349,"value":1197},", so neither of us can forget.",{"type":339,"tag":750,"props":1199,"children":1201},{"id":1200},"rule-3-decide-what-the-model-cant-see-before-deciding-what-it-can-do",[1202],{"type":349,"value":1203},"Rule 3: decide what the model can't see before deciding what it can do",{"type":339,"tag":345,"props":1205,"children":1206},{},[1207],{"type":349,"value":1208},"The deny list was the first thing in the spec, and it shaped every later decision. This is the whole of the project's Claude Code settings:",{"type":339,"tag":820,"props":1210,"children":1213},{"className":1211,"code":1212,"language":18,"meta":8,"style":8},"language-json shiki shiki-themes github-dark","{\n  \"permissions\": {\n    \"deny\": [\n      \"Read(./journal/**)\",\n      \"Read(./health/**)\",\n      \"Read(./sources/health/**)\",\n      \"Read(./inbox/**)\",\n      \"Read(./.secrets/**)\"\n    ]\n  },\n  \"hooks\": {\n    \"PostToolUse\": [\n      {\n        \"matcher\": \"Write|Edit\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"/usr/bin/env python3 /Users/marcoendrizzi/life/sync/log_change.py\"\n          }\n        ]\n      }\n    ]\n  },\n  \"sandbox\": {\n    \"enabled\": true,\n    \"allowUnsandboxedCommands\": false,\n    \"filesystem\": {\n      \"denyRead\": [\n        \"/Users/marcoendrizzi/life/journal/\",\n        \"/Users/marcoendrizzi/life/health/\",\n        \"/Users/marcoendrizzi/life/sources/health/\",\n        \"/Users/marcoendrizzi/life/inbox/\",\n        \"/Users/marcoendrizzi/life/.secrets/\"\n      ]\n    }\n  }\n}\n",[1214],{"type":339,"tag":398,"props":1215,"children":1216},{"__ignoreMap":8},[1217,1226,1240,1253,1267,1279,1291,1303,1311,1319,1327,1339,1351,1359,1381,1393,1401,1422,1439,1447,1456,1465,1473,1481,1494,1515,1537,1550,1563,1576,1589,1602,1615,1624,1633,1642,1651],{"type":339,"tag":830,"props":1218,"children":1219},{"class":832,"line":108},[1220],{"type":339,"tag":830,"props":1221,"children":1223},{"style":1222},"--shiki-default:#E1E4E8",[1224],{"type":349,"value":1225},"{\n",{"type":339,"tag":830,"props":1227,"children":1228},{"class":832,"line":129},[1229,1235],{"type":339,"tag":830,"props":1230,"children":1232},{"style":1231},"--shiki-default:#79B8FF",[1233],{"type":349,"value":1234},"  \"permissions\"",{"type":339,"tag":830,"props":1236,"children":1237},{"style":1222},[1238],{"type":349,"value":1239},": {\n",{"type":339,"tag":830,"props":1241,"children":1242},{"class":832,"line":151},[1243,1248],{"type":339,"tag":830,"props":1244,"children":1245},{"style":1231},[1246],{"type":349,"value":1247},"    \"deny\"",{"type":339,"tag":830,"props":1249,"children":1250},{"style":1222},[1251],{"type":349,"value":1252},": [\n",{"type":339,"tag":830,"props":1254,"children":1255},{"class":832,"line":173},[1256,1262],{"type":339,"tag":830,"props":1257,"children":1259},{"style":1258},"--shiki-default:#9ECBFF",[1260],{"type":349,"value":1261},"      \"Read(./journal/**)\"",{"type":339,"tag":830,"props":1263,"children":1264},{"style":1222},[1265],{"type":349,"value":1266},",\n",{"type":339,"tag":830,"props":1268,"children":1269},{"class":832,"line":140},[1270,1275],{"type":339,"tag":830,"props":1271,"children":1272},{"style":1258},[1273],{"type":349,"value":1274},"      \"Read(./health/**)\"",{"type":339,"tag":830,"props":1276,"children":1277},{"style":1222},[1278],{"type":349,"value":1266},{"type":339,"tag":830,"props":1280,"children":1281},{"class":832,"line":162},[1282,1287],{"type":339,"tag":830,"props":1283,"children":1284},{"style":1258},[1285],{"type":349,"value":1286},"      \"Read(./sources/health/**)\"",{"type":339,"tag":830,"props":1288,"children":1289},{"style":1222},[1290],{"type":349,"value":1266},{"type":339,"tag":830,"props":1292,"children":1293},{"class":832,"line":184},[1294,1299],{"type":339,"tag":830,"props":1295,"children":1296},{"style":1258},[1297],{"type":349,"value":1298},"      \"Read(./inbox/**)\"",{"type":339,"tag":830,"props":1300,"children":1301},{"style":1222},[1302],{"type":349,"value":1266},{"type":339,"tag":830,"props":1304,"children":1305},{"class":832,"line":49},[1306],{"type":339,"tag":830,"props":1307,"children":1308},{"style":1258},[1309],{"type":349,"value":1310},"      \"Read(./.secrets/**)\"\n",{"type":339,"tag":830,"props":1312,"children":1313},{"class":832,"line":70},[1314],{"type":339,"tag":830,"props":1315,"children":1316},{"style":1222},[1317],{"type":349,"value":1318},"    ]\n",{"type":339,"tag":830,"props":1320,"children":1321},{"class":832,"line":60},[1322],{"type":339,"tag":830,"props":1323,"children":1324},{"style":1222},[1325],{"type":349,"value":1326},"  },\n",{"type":339,"tag":830,"props":1328,"children":1329},{"class":832,"line":81},[1330,1335],{"type":339,"tag":830,"props":1331,"children":1332},{"style":1231},[1333],{"type":349,"value":1334},"  \"hooks\"",{"type":339,"tag":830,"props":1336,"children":1337},{"style":1222},[1338],{"type":349,"value":1239},{"type":339,"tag":830,"props":1340,"children":1341},{"class":832,"line":90},[1342,1347],{"type":339,"tag":830,"props":1343,"children":1344},{"style":1231},[1345],{"type":349,"value":1346},"    \"PostToolUse\"",{"type":339,"tag":830,"props":1348,"children":1349},{"style":1222},[1350],{"type":349,"value":1252},{"type":339,"tag":830,"props":1352,"children":1353},{"class":832,"line":15},[1354],{"type":339,"tag":830,"props":1355,"children":1356},{"style":1222},[1357],{"type":349,"value":1358},"      {\n",{"type":339,"tag":830,"props":1360,"children":1361},{"class":832,"line":28},[1362,1367,1372,1377],{"type":339,"tag":830,"props":1363,"children":1364},{"style":1231},[1365],{"type":349,"value":1366},"        \"matcher\"",{"type":339,"tag":830,"props":1368,"children":1369},{"style":1222},[1370],{"type":349,"value":1371},": ",{"type":339,"tag":830,"props":1373,"children":1374},{"style":1258},[1375],{"type":349,"value":1376},"\"Write|Edit\"",{"type":339,"tag":830,"props":1378,"children":1379},{"style":1222},[1380],{"type":349,"value":1266},{"type":339,"tag":830,"props":1382,"children":1383},{"class":832,"line":221},[1384,1389],{"type":339,"tag":830,"props":1385,"children":1386},{"style":1231},[1387],{"type":349,"value":1388},"        \"hooks\"",{"type":339,"tag":830,"props":1390,"children":1391},{"style":1222},[1392],{"type":349,"value":1252},{"type":339,"tag":830,"props":1394,"children":1395},{"class":832,"line":232},[1396],{"type":339,"tag":830,"props":1397,"children":1398},{"style":1222},[1399],{"type":349,"value":1400},"          {\n",{"type":339,"tag":830,"props":1402,"children":1403},{"class":832,"line":196},[1404,1409,1413,1418],{"type":339,"tag":830,"props":1405,"children":1406},{"style":1231},[1407],{"type":349,"value":1408},"            \"type\"",{"type":339,"tag":830,"props":1410,"children":1411},{"style":1222},[1412],{"type":349,"value":1371},{"type":339,"tag":830,"props":1414,"children":1415},{"style":1258},[1416],{"type":349,"value":1417},"\"command\"",{"type":339,"tag":830,"props":1419,"children":1420},{"style":1222},[1421],{"type":349,"value":1266},{"type":339,"tag":830,"props":1423,"children":1424},{"class":832,"line":209},[1425,1430,1434],{"type":339,"tag":830,"props":1426,"children":1427},{"style":1231},[1428],{"type":349,"value":1429},"            \"command\"",{"type":339,"tag":830,"props":1431,"children":1432},{"style":1222},[1433],{"type":349,"value":1371},{"type":339,"tag":830,"props":1435,"children":1436},{"style":1258},[1437],{"type":349,"value":1438},"\"/usr/bin/env python3 /Users/marcoendrizzi/life/sync/log_change.py\"\n",{"type":339,"tag":830,"props":1440,"children":1441},{"class":832,"line":243},[1442],{"type":339,"tag":830,"props":1443,"children":1444},{"style":1222},[1445],{"type":349,"value":1446},"          }\n",{"type":339,"tag":830,"props":1448,"children":1450},{"class":832,"line":1449},20,[1451],{"type":339,"tag":830,"props":1452,"children":1453},{"style":1222},[1454],{"type":349,"value":1455},"        ]\n",{"type":339,"tag":830,"props":1457,"children":1459},{"class":832,"line":1458},21,[1460],{"type":339,"tag":830,"props":1461,"children":1462},{"style":1222},[1463],{"type":349,"value":1464},"      }\n",{"type":339,"tag":830,"props":1466,"children":1468},{"class":832,"line":1467},22,[1469],{"type":339,"tag":830,"props":1470,"children":1471},{"style":1222},[1472],{"type":349,"value":1318},{"type":339,"tag":830,"props":1474,"children":1476},{"class":832,"line":1475},23,[1477],{"type":339,"tag":830,"props":1478,"children":1479},{"style":1222},[1480],{"type":349,"value":1326},{"type":339,"tag":830,"props":1482,"children":1484},{"class":832,"line":1483},24,[1485,1490],{"type":339,"tag":830,"props":1486,"children":1487},{"style":1231},[1488],{"type":349,"value":1489},"  \"sandbox\"",{"type":339,"tag":830,"props":1491,"children":1492},{"style":1222},[1493],{"type":349,"value":1239},{"type":339,"tag":830,"props":1495,"children":1497},{"class":832,"line":1496},25,[1498,1503,1507,1511],{"type":339,"tag":830,"props":1499,"children":1500},{"style":1231},[1501],{"type":349,"value":1502},"    \"enabled\"",{"type":339,"tag":830,"props":1504,"children":1505},{"style":1222},[1506],{"type":349,"value":1371},{"type":339,"tag":830,"props":1508,"children":1509},{"style":1231},[1510],{"type":349,"value":390},{"type":339,"tag":830,"props":1512,"children":1513},{"style":1222},[1514],{"type":349,"value":1266},{"type":339,"tag":830,"props":1516,"children":1518},{"class":832,"line":1517},26,[1519,1524,1528,1533],{"type":339,"tag":830,"props":1520,"children":1521},{"style":1231},[1522],{"type":349,"value":1523},"    \"allowUnsandboxedCommands\"",{"type":339,"tag":830,"props":1525,"children":1526},{"style":1222},[1527],{"type":349,"value":1371},{"type":339,"tag":830,"props":1529,"children":1530},{"style":1231},[1531],{"type":349,"value":1532},"false",{"type":339,"tag":830,"props":1534,"children":1535},{"style":1222},[1536],{"type":349,"value":1266},{"type":339,"tag":830,"props":1538,"children":1540},{"class":832,"line":1539},27,[1541,1546],{"type":339,"tag":830,"props":1542,"children":1543},{"style":1231},[1544],{"type":349,"value":1545},"    \"filesystem\"",{"type":339,"tag":830,"props":1547,"children":1548},{"style":1222},[1549],{"type":349,"value":1239},{"type":339,"tag":830,"props":1551,"children":1553},{"class":832,"line":1552},28,[1554,1559],{"type":339,"tag":830,"props":1555,"children":1556},{"style":1231},[1557],{"type":349,"value":1558},"      \"denyRead\"",{"type":339,"tag":830,"props":1560,"children":1561},{"style":1222},[1562],{"type":349,"value":1252},{"type":339,"tag":830,"props":1564,"children":1566},{"class":832,"line":1565},29,[1567,1572],{"type":339,"tag":830,"props":1568,"children":1569},{"style":1258},[1570],{"type":349,"value":1571},"        \"/Users/marcoendrizzi/life/journal/\"",{"type":339,"tag":830,"props":1573,"children":1574},{"style":1222},[1575],{"type":349,"value":1266},{"type":339,"tag":830,"props":1577,"children":1579},{"class":832,"line":1578},30,[1580,1585],{"type":339,"tag":830,"props":1581,"children":1582},{"style":1258},[1583],{"type":349,"value":1584},"        \"/Users/marcoendrizzi/life/health/\"",{"type":339,"tag":830,"props":1586,"children":1587},{"style":1222},[1588],{"type":349,"value":1266},{"type":339,"tag":830,"props":1590,"children":1592},{"class":832,"line":1591},31,[1593,1598],{"type":339,"tag":830,"props":1594,"children":1595},{"style":1258},[1596],{"type":349,"value":1597},"        \"/Users/marcoendrizzi/life/sources/health/\"",{"type":339,"tag":830,"props":1599,"children":1600},{"style":1222},[1601],{"type":349,"value":1266},{"type":339,"tag":830,"props":1603,"children":1605},{"class":832,"line":1604},32,[1606,1611],{"type":339,"tag":830,"props":1607,"children":1608},{"style":1258},[1609],{"type":349,"value":1610},"        \"/Users/marcoendrizzi/life/inbox/\"",{"type":339,"tag":830,"props":1612,"children":1613},{"style":1222},[1614],{"type":349,"value":1266},{"type":339,"tag":830,"props":1616,"children":1618},{"class":832,"line":1617},33,[1619],{"type":339,"tag":830,"props":1620,"children":1621},{"style":1258},[1622],{"type":349,"value":1623},"        \"/Users/marcoendrizzi/life/.secrets/\"\n",{"type":339,"tag":830,"props":1625,"children":1627},{"class":832,"line":1626},34,[1628],{"type":339,"tag":830,"props":1629,"children":1630},{"style":1222},[1631],{"type":349,"value":1632},"      ]\n",{"type":339,"tag":830,"props":1634,"children":1636},{"class":832,"line":1635},35,[1637],{"type":339,"tag":830,"props":1638,"children":1639},{"style":1222},[1640],{"type":349,"value":1641},"    }\n",{"type":339,"tag":830,"props":1643,"children":1645},{"class":832,"line":1644},36,[1646],{"type":339,"tag":830,"props":1647,"children":1648},{"style":1222},[1649],{"type":349,"value":1650},"  }\n",{"type":339,"tag":830,"props":1652,"children":1654},{"class":832,"line":1653},37,[1655],{"type":339,"tag":830,"props":1656,"children":1657},{"style":1222},[1658],{"type":349,"value":1168},{"type":339,"tag":345,"props":1660,"children":1661},{},[1662],{"type":349,"value":1663},"Notice the same five paths appear twice. That is because the layers are not interchangeable, and I learned it the hard way.",{"type":339,"tag":345,"props":1665,"children":1666},{},[1667,1669,1675,1677,1683,1685,1698],{"type":349,"value":1668},"The ",{"type":339,"tag":398,"props":1670,"children":1672},{"className":1671},[],[1673],{"type":349,"value":1674},"permissions.deny",{"type":349,"value":1676}," block stops the Read, Glob and Grep tools. It does nothing about the shell. A deny rule on Read does not stop ",{"type":339,"tag":398,"props":1678,"children":1680},{"className":1679},[],[1681],{"type":349,"value":1682},"cat",{"type":349,"value":1684},". The ",{"type":339,"tag":762,"props":1686,"children":1689},{"href":1687,"rel":1688},"https://code.claude.com/docs/en/sandboxing",[766],[1690,1692],{"type":349,"value":1691},"sandbox ",{"type":339,"tag":398,"props":1693,"children":1695},{"className":1694},[],[1696],{"type":349,"value":1697},"denyRead",{"type":349,"value":1699}," list closes that gap at the OS level, so a shell read now fails with \"Operation not permitted\". The scripts that legitimately need those files run from launchd, outside the sandbox.",{"type":339,"tag":345,"props":1701,"children":1702},{},[1703,1705,1712,1714,1720,1722,1728],{"type":349,"value":1704},"Underneath both, ",{"type":339,"tag":762,"props":1706,"children":1709},{"href":1707,"rel":1708},"https://github.com/AGWA/git-crypt",[766],[1710],{"type":349,"value":1711},"git-crypt",{"type":349,"value":1713}," encrypts ",{"type":339,"tag":398,"props":1715,"children":1717},{"className":1716},[],[1718],{"type":349,"value":1719},"journal/",{"type":349,"value":1721}," and ",{"type":339,"tag":398,"props":1723,"children":1725},{"className":1724},[],[1726],{"type":349,"value":1727},"health/",{"type":349,"value":1729}," at rest so the remote only ever holds ciphertext. That protects against someone getting into the repo. It does nothing about a process on my own machine, because any key I can use, any other local process can use. FileVault covers the laptop being stolen.",{"type":339,"tag":345,"props":1731,"children":1732},{},[1733],{"type":349,"value":1734},"The lesson that cost me the most is that a rule you have written down is not a rule that is running. Project settings only load when the session starts inside the project. For several days I ran sessions from a different directory, the rules never applied, and I described the protection as active while it was theatre. The schema file now has this section, and it is the one I would copy into any agent-maintained project:",{"type":339,"tag":1736,"props":1737,"children":1738},"blockquote",{},[1739],{"type":339,"tag":345,"props":1740,"children":1741},{},[1742],{"type":349,"value":1743},"\"Working\" requires a command and its output, not the act of having written the file. Before saying something is working: run the thing, show the output, and prefer a check that would fail if the claim were false. A grep that matches a stale line is not evidence.",{"type":339,"tag":750,"props":1745,"children":1747},{"id":1746},"rule-4-the-private-stuff-gets-a-local-model",[1748],{"type":349,"value":1749},"Rule 4: the private stuff gets a local model",{"type":339,"tag":345,"props":1751,"children":1752},{},[1753,1755,1762],{"type":349,"value":1754},"The journal is the one thing no cloud model touches. Every night at three a local model reads the entries that have not been processed yet and proposes tasks, people mentions and ideas. It is Qwen 3 at eight billion parameters through ",{"type":339,"tag":762,"props":1756,"children":1759},{"href":1757,"rel":1758},"https://ollama.com",[766],[1760],{"type":349,"value":1761},"Ollama",{"type":349,"value":1763},", and the call looks like this:",{"type":339,"tag":820,"props":1765,"children":1767},{"className":1066,"code":1766,"language":1068,"meta":8,"style":8},"OLLAMA = \"http://localhost:11434/api/chat\"\nMODEL = \"qwen3:8b\"\n\n# Enforced by Ollama rather than requested in prose. A model that cannot emit the wrong\n# shape does not need to be trusted to emit the right one.\nSCHEMA = {\n    \"type\": \"object\",\n    \"properties\": {\n        \"tasks\": {\"type\": \"array\", \"items\": {\"...\": \"...\"}},\n        \"people\": {\"type\": \"array\", \"items\": {\"...\": \"...\"}},\n        \"ideas\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n        \"uncertain\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n    },\n    \"required\": [\"tasks\", \"people\", \"ideas\", \"uncertain\"],\n}\n\ndef distil(entry_text):\n    body = json.dumps({\n        \"model\": MODEL,\n        \"messages\": [{\"role\": \"user\",\n                      \"content\": prompt_text() + \"\\n\\n---\\n\\n\" + entry_text}],\n        \"format\": SCHEMA,\n        \"stream\": False,\n        \"think\": True,\n        \"options\": {\"temperature\": 0},\n    }).encode()\n    req = urllib.request.Request(OLLAMA, data=body,\n                                 headers={\"Content-Type\": \"application/json\"})\n    with urllib.request.urlopen(req, timeout=1200) as r:\n        content = json.load(r)[\"message\"][\"content\"]\n    return json.loads(content)\n",[1768],{"type":339,"tag":398,"props":1769,"children":1770},{"__ignoreMap":8},[1771,1779,1787,1795,1803,1811,1819,1827,1835,1843,1851,1859,1867,1875,1883,1890,1897,1905,1913,1921,1929,1937,1945,1953,1961,1969,1977,1985,1993,2001,2009],{"type":339,"tag":830,"props":1772,"children":1773},{"class":832,"line":108},[1774],{"type":339,"tag":830,"props":1775,"children":1776},{},[1777],{"type":349,"value":1778},"OLLAMA = \"http://localhost:11434/api/chat\"\n",{"type":339,"tag":830,"props":1780,"children":1781},{"class":832,"line":129},[1782],{"type":339,"tag":830,"props":1783,"children":1784},{},[1785],{"type":349,"value":1786},"MODEL = \"qwen3:8b\"\n",{"type":339,"tag":830,"props":1788,"children":1789},{"class":832,"line":151},[1790],{"type":339,"tag":830,"props":1791,"children":1792},{"emptyLinePlaceholder":641},[1793],{"type":349,"value":1794},"\n",{"type":339,"tag":830,"props":1796,"children":1797},{"class":832,"line":173},[1798],{"type":339,"tag":830,"props":1799,"children":1800},{},[1801],{"type":349,"value":1802},"# Enforced by Ollama rather than requested in prose. A model that cannot emit the wrong\n",{"type":339,"tag":830,"props":1804,"children":1805},{"class":832,"line":140},[1806],{"type":339,"tag":830,"props":1807,"children":1808},{},[1809],{"type":349,"value":1810},"# shape does not need to be trusted to emit the right one.\n",{"type":339,"tag":830,"props":1812,"children":1813},{"class":832,"line":162},[1814],{"type":339,"tag":830,"props":1815,"children":1816},{},[1817],{"type":349,"value":1818},"SCHEMA = {\n",{"type":339,"tag":830,"props":1820,"children":1821},{"class":832,"line":184},[1822],{"type":339,"tag":830,"props":1823,"children":1824},{},[1825],{"type":349,"value":1826},"    \"type\": \"object\",\n",{"type":339,"tag":830,"props":1828,"children":1829},{"class":832,"line":49},[1830],{"type":339,"tag":830,"props":1831,"children":1832},{},[1833],{"type":349,"value":1834},"    \"properties\": {\n",{"type":339,"tag":830,"props":1836,"children":1837},{"class":832,"line":70},[1838],{"type":339,"tag":830,"props":1839,"children":1840},{},[1841],{"type":349,"value":1842},"        \"tasks\": {\"type\": \"array\", \"items\": {\"...\": \"...\"}},\n",{"type":339,"tag":830,"props":1844,"children":1845},{"class":832,"line":60},[1846],{"type":339,"tag":830,"props":1847,"children":1848},{},[1849],{"type":349,"value":1850},"        \"people\": {\"type\": \"array\", \"items\": {\"...\": \"...\"}},\n",{"type":339,"tag":830,"props":1852,"children":1853},{"class":832,"line":81},[1854],{"type":339,"tag":830,"props":1855,"children":1856},{},[1857],{"type":349,"value":1858},"        \"ideas\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n",{"type":339,"tag":830,"props":1860,"children":1861},{"class":832,"line":90},[1862],{"type":339,"tag":830,"props":1863,"children":1864},{},[1865],{"type":349,"value":1866},"        \"uncertain\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n",{"type":339,"tag":830,"props":1868,"children":1869},{"class":832,"line":15},[1870],{"type":339,"tag":830,"props":1871,"children":1872},{},[1873],{"type":349,"value":1874},"    },\n",{"type":339,"tag":830,"props":1876,"children":1877},{"class":832,"line":28},[1878],{"type":339,"tag":830,"props":1879,"children":1880},{},[1881],{"type":349,"value":1882},"    \"required\": [\"tasks\", \"people\", \"ideas\", \"uncertain\"],\n",{"type":339,"tag":830,"props":1884,"children":1885},{"class":832,"line":221},[1886],{"type":339,"tag":830,"props":1887,"children":1888},{},[1889],{"type":349,"value":1168},{"type":339,"tag":830,"props":1891,"children":1892},{"class":832,"line":232},[1893],{"type":339,"tag":830,"props":1894,"children":1895},{"emptyLinePlaceholder":641},[1896],{"type":349,"value":1794},{"type":339,"tag":830,"props":1898,"children":1899},{"class":832,"line":196},[1900],{"type":339,"tag":830,"props":1901,"children":1902},{},[1903],{"type":349,"value":1904},"def distil(entry_text):\n",{"type":339,"tag":830,"props":1906,"children":1907},{"class":832,"line":209},[1908],{"type":339,"tag":830,"props":1909,"children":1910},{},[1911],{"type":349,"value":1912},"    body = json.dumps({\n",{"type":339,"tag":830,"props":1914,"children":1915},{"class":832,"line":243},[1916],{"type":339,"tag":830,"props":1917,"children":1918},{},[1919],{"type":349,"value":1920},"        \"model\": MODEL,\n",{"type":339,"tag":830,"props":1922,"children":1923},{"class":832,"line":1449},[1924],{"type":339,"tag":830,"props":1925,"children":1926},{},[1927],{"type":349,"value":1928},"        \"messages\": [{\"role\": \"user\",\n",{"type":339,"tag":830,"props":1930,"children":1931},{"class":832,"line":1458},[1932],{"type":339,"tag":830,"props":1933,"children":1934},{},[1935],{"type":349,"value":1936},"                      \"content\": prompt_text() + \"\\n\\n---\\n\\n\" + entry_text}],\n",{"type":339,"tag":830,"props":1938,"children":1939},{"class":832,"line":1467},[1940],{"type":339,"tag":830,"props":1941,"children":1942},{},[1943],{"type":349,"value":1944},"        \"format\": SCHEMA,\n",{"type":339,"tag":830,"props":1946,"children":1947},{"class":832,"line":1475},[1948],{"type":339,"tag":830,"props":1949,"children":1950},{},[1951],{"type":349,"value":1952},"        \"stream\": False,\n",{"type":339,"tag":830,"props":1954,"children":1955},{"class":832,"line":1483},[1956],{"type":339,"tag":830,"props":1957,"children":1958},{},[1959],{"type":349,"value":1960},"        \"think\": True,\n",{"type":339,"tag":830,"props":1962,"children":1963},{"class":832,"line":1496},[1964],{"type":339,"tag":830,"props":1965,"children":1966},{},[1967],{"type":349,"value":1968},"        \"options\": {\"temperature\": 0},\n",{"type":339,"tag":830,"props":1970,"children":1971},{"class":832,"line":1517},[1972],{"type":339,"tag":830,"props":1973,"children":1974},{},[1975],{"type":349,"value":1976},"    }).encode()\n",{"type":339,"tag":830,"props":1978,"children":1979},{"class":832,"line":1539},[1980],{"type":339,"tag":830,"props":1981,"children":1982},{},[1983],{"type":349,"value":1984},"    req = urllib.request.Request(OLLAMA, data=body,\n",{"type":339,"tag":830,"props":1986,"children":1987},{"class":832,"line":1552},[1988],{"type":339,"tag":830,"props":1989,"children":1990},{},[1991],{"type":349,"value":1992},"                                 headers={\"Content-Type\": \"application/json\"})\n",{"type":339,"tag":830,"props":1994,"children":1995},{"class":832,"line":1565},[1996],{"type":339,"tag":830,"props":1997,"children":1998},{},[1999],{"type":349,"value":2000},"    with urllib.request.urlopen(req, timeout=1200) as r:\n",{"type":339,"tag":830,"props":2002,"children":2003},{"class":832,"line":1578},[2004],{"type":339,"tag":830,"props":2005,"children":2006},{},[2007],{"type":349,"value":2008},"        content = json.load(r)[\"message\"][\"content\"]\n",{"type":339,"tag":830,"props":2010,"children":2011},{"class":832,"line":1591},[2012],{"type":339,"tag":830,"props":2013,"children":2014},{},[2015],{"type":349,"value":2016},"    return json.loads(content)\n",{"type":339,"tag":345,"props":2018,"children":2019},{},[2020,2022,2028,2030,2037],{"type":349,"value":2021},"Three things to copy. The schema goes in the ",{"type":339,"tag":398,"props":2023,"children":2025},{"className":2024},[],[2026],{"type":349,"value":2027},"format",{"type":349,"value":2029}," field, so Ollama ",{"type":339,"tag":762,"props":2031,"children":2034},{"href":2032,"rel":2033},"https://ollama.com/blog/structured-outputs",[766],[2035],{"type":349,"value":2036},"constrains the output",{"type":349,"value":2038}," and the model cannot return the wrong shape. Reasoning is left on, because with it off the model kept turning things I had already done into tasks. And the model has no filesystem access at all. Text in, JSON out, Python does every write. The proposals land in the inbox as checkboxes and nothing reaches Habitica or a people page until I tick it. The tick is the human gate on what leaves the machine.",{"type":339,"tag":345,"props":2040,"children":2041},{},[2042],{"type":349,"value":2043},"This is also why processed entries are tracked in a state file rather than a frontmatter stamp. The journal is immutable, even to the script that reads it.",{"type":339,"tag":750,"props":2045,"children":2047},{"id":2046},"integration-notes",[2048],{"type":349,"value":2049},"Integration notes",{"type":339,"tag":345,"props":2051,"children":2052},{},[2053,2055,2061],{"type":349,"value":2054},"The syncs are boring by design, but each API had one thing I wish I had known. Secrets live in the macOS Keychain and the scripts read them with ",{"type":339,"tag":398,"props":2056,"children":2058},{"className":2057},[],[2059],{"type":349,"value":2060},"security find-generic-password",{"type":349,"value":2062},".",{"type":339,"tag":2064,"props":2065,"children":2066},"ul",{},[2067,2084,2136,2159],{"type":339,"tag":2068,"props":2069,"children":2070},"li",{},[2071,2082],{"type":339,"tag":2072,"props":2073,"children":2074},"strong",{},[2075],{"type":339,"tag":762,"props":2076,"children":2079},{"href":2077,"rel":2078},"https://docs.monzo.com/",[766],[2080],{"type":349,"value":2081},"Monzo",{"type":349,"value":2083}," gives you full history for five minutes after you approve the app on your phone, then only the trailing ninety days. Asking for older returns a 403, not an empty list, so the daily sync clamps to eighty-nine days. Refresh tokens rotate on every use, so persist the new one before making any other call.",{"type":339,"tag":2068,"props":2085,"children":2086},{},[2087,2097,2099,2110,2112,2118,2120,2126,2128,2134],{"type":339,"tag":2072,"props":2088,"children":2089},{},[2090],{"type":339,"tag":762,"props":2091,"children":2094},{"href":2092,"rel":2093},"https://developers.google.com/health",[766],[2095],{"type":349,"value":2096},"Google Health",{"type":349,"value":2098}," replaced Fitbit's Web API. Use the ",{"type":339,"tag":762,"props":2100,"children":2103},{"href":2101,"rel":2102},"https://developers.google.com/health/reference/rest/v4/users.dataTypes.dataPoints/reconcile",[766],[2104],{"type":339,"tag":398,"props":2105,"children":2107},{"className":2106},[],[2108],{"type":349,"value":2109},"reconcile",{"type":349,"value":2111}," endpoint for daily values, because ",{"type":339,"tag":398,"props":2113,"children":2115},{"className":2114},[],[2116],{"type":349,"value":2117},"list",{"type":349,"value":2119}," returns one point per source and a watch plus Health Connect double-counts a day. Roll-ups answer under ",{"type":339,"tag":398,"props":2121,"children":2123},{"className":2122},[],[2124],{"type":349,"value":2125},"rollupDataPoints",{"type":349,"value":2127},", everything else under ",{"type":339,"tag":398,"props":2129,"children":2131},{"className":2130},[],[2132],{"type":349,"value":2133},"dataPoints",{"type":349,"value":2135},", and reading the wrong key gives zero rows and no error. That is how a year of steps came back empty once. Also, int64 arrives as a string, and a refresh token dies after seven days while your consent screen is in Testing.",{"type":339,"tag":2068,"props":2137,"children":2138},{},[2139,2149,2151,2157],{"type":339,"tag":2072,"props":2140,"children":2141},{},[2142],{"type":339,"tag":762,"props":2143,"children":2146},{"href":2144,"rel":2145},"https://apidoc.habitica.com/",[766],[2147],{"type":349,"value":2148},"Habitica",{"type":349,"value":2150}," needs an ",{"type":339,"tag":398,"props":2152,"children":2154},{"className":2153},[],[2155],{"type":349,"value":2156},"x-client",{"type":349,"value":2158}," header on every authenticated call since mid 2025, and the error when you omit it looks like a bad credential. Completed to-dos vanish from the main list and must be fetched separately. Everything is pushed as a to-do, never a daily, because dailies punish a missed day.",{"type":339,"tag":2068,"props":2160,"children":2161},{},[2162,2167],{"type":339,"tag":2072,"props":2163,"children":2164},{},[2165],{"type":349,"value":2166},"Trading 212",{"type":349,"value":2168}," reports profit in the account currency but prices in the instrument's currency. For a while every US holding wore a pound sign.",{"type":339,"tag":750,"props":2170,"children":2172},{"id":2171},"if-you-build-one",[2173],{"type":349,"value":2174},"If you build one",{"type":339,"tag":345,"props":2176,"children":2177},{},[2178],{"type":349,"value":2179},"Build the plumbing first. I built the finance pages before the lint, the log and the hook, and for two weeks the hygiene fell to whoever remembered, which turned out to be nobody. Decide the no-go zones before the capabilities. Verify every claim of \"working\" with a command that would fail if it were false. And keep the log. It is the only reason I trust anything in the folder, and when a number looks wrong, half the time the answer is me.",{"type":339,"tag":345,"props":2181,"children":2182},{},[2183],{"type":349,"value":2184},"The AI does not run my life. It keeps the ledger, and it keeps it out of my head. That turns out to be most of what I wanted.",{"type":339,"tag":2186,"props":2187,"children":2188},"style",{},[2189],{"type":349,"value":2190},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":8,"searchDepth":129,"depth":129,"links":2192},[2193,2194,2195,2196,2197,2198,2199],{"id":752,"depth":129,"text":755},{"id":796,"depth":129,"text":799},{"id":974,"depth":129,"text":977},{"id":1200,"depth":129,"text":1203},{"id":1746,"depth":129,"text":1749},{"id":2046,"depth":129,"text":2049},{"id":2171,"depth":129,"text":2174},"content:articles:how-i-let-ai-run-my-life.md","articles/how-i-let-ai-run-my-life.md","articles/how-i-let-ai-run-my-life",{"_path":2204,"_dir":724,"_draft":7,"_partial":7,"_locale":8,"title":2205,"description":2206,"published":2207,"slug":2208,"body":2209,"_type":440,"_id":3818,"_source":19,"_file":3819,"_stem":3820,"_extension":444},"/articles/serverless-on-aws-without-a-serverless-framework","Serverless on AWS without a serverless framework","Terraform owns every resource, the frontend gets its config from Terraform outputs, Lambda functions are pnpm workspace packages, and one zod contract drives the API gateway, the client and the handlers. The template I start every serverless project from, and the parts worth stealing.","2026/7/27","serverless-on-aws-without-a-serverless-framework",{"type":336,"children":2210,"toc":3807},[2211,2216,2221,2227,2239,2247,2252,2329,2335,2348,2361,2423,2526,2587,2600,2606,2627,2640,2766,2787,2822,2828,2841,3066,3071,3079,3092,3237,3250,3562,3591,3612,3618,3641,3671,3679,3690,3711,3716,3729,3735,3762,3768,3781,3787,3792,3798,3803],{"type":339,"tag":345,"props":2212,"children":2213},{},[2214],{"type":349,"value":2215},"Every serverless framework I have used wants to own the infrastructure. SST, the Serverless Framework, SAM, Amplify Gen 2. They are all good at the first week and all painful in month six, when you need a resource the framework did not anticipate and you end up with two sources of truth for one AWS account.",{"type":339,"tag":345,"props":2217,"children":2218},{},[2219],{"type":349,"value":2220},"So the template I start projects from at Storm Reply UK does it the other way round. Terraform owns every resource. The frontend is told where things are by Terraform outputs. Lambda functions are ordinary packages in a pnpm monorepo and Terraform builds them. There is no framework in the middle, and after a year of using it on real projects I would not go back. This post is the four ideas that make it work and the rough edges I have not sanded off.",{"type":339,"tag":750,"props":2222,"children":2224},{"id":2223},"what-it-deploys",[2225],{"type":349,"value":2226},"What it deploys",{"type":339,"tag":345,"props":2228,"children":2229},{},[2230,2232,2237],{"type":349,"value":2231},"Two React SPAs on CloudFront, Cognito for auth, an API Gateway REST API with a Lambda authorizer, one Lambda per API path, two DynamoDB tables, a Bedrock knowledge base for retrieval, and the usual monitoring, budget and DNS. The second SPA is a CloudWatch dashboard gated to a ",{"type":339,"tag":398,"props":2233,"children":2235},{"className":2234},[],[2236],{"type":349,"value":298},{"type":349,"value":2238}," Cognito group, which reads metrics directly using credentials from the identity pool.",{"type":339,"tag":345,"props":2240,"children":2241},{},[2242],{"type":339,"tag":784,"props":2243,"children":2246},{"alt":2244,"src":2245},"Architecture: CloudFront and S3 serve two React SPAs, Cognito issues tokens, API Gateway checks them with a Lambda authorizer and routes each path to its own Lambda, which talks to DynamoDB or Bedrock. Terraform owns every box and its outputs feed the frontend config","/articles/serverless-toolkit-architecture.svg",[],{"type":339,"tag":345,"props":2248,"children":2249},{},[2250],{"type":349,"value":2251},"The monorepo layout is what you would expect, with one addition that matters and gets its own section below:",{"type":339,"tag":820,"props":2253,"children":2257},{"className":2254,"code":2255,"language":2256,"meta":8,"style":8},"language-yaml shiki shiki-themes github-dark","# pnpm-workspace.yaml\npackages:\n  - \"apps/*\"\n  - \"packages/*\"\n  # API lambda functions are workspace packages too, so they can import shared packages\n  - \"apps/web/functions/*\"\n","yaml",[2258],{"type":339,"tag":398,"props":2259,"children":2260},{"__ignoreMap":8},[2261,2270,2284,2297,2309,2317],{"type":339,"tag":830,"props":2262,"children":2263},{"class":832,"line":108},[2264],{"type":339,"tag":830,"props":2265,"children":2267},{"style":2266},"--shiki-default:#6A737D",[2268],{"type":349,"value":2269},"# pnpm-workspace.yaml\n",{"type":339,"tag":830,"props":2271,"children":2272},{"class":832,"line":129},[2273,2279],{"type":339,"tag":830,"props":2274,"children":2276},{"style":2275},"--shiki-default:#85E89D",[2277],{"type":349,"value":2278},"packages",{"type":339,"tag":830,"props":2280,"children":2281},{"style":1222},[2282],{"type":349,"value":2283},":\n",{"type":339,"tag":830,"props":2285,"children":2286},{"class":832,"line":151},[2287,2292],{"type":339,"tag":830,"props":2288,"children":2289},{"style":1222},[2290],{"type":349,"value":2291},"  - ",{"type":339,"tag":830,"props":2293,"children":2294},{"style":1258},[2295],{"type":349,"value":2296},"\"apps/*\"\n",{"type":339,"tag":830,"props":2298,"children":2299},{"class":832,"line":173},[2300,2304],{"type":339,"tag":830,"props":2301,"children":2302},{"style":1222},[2303],{"type":349,"value":2291},{"type":339,"tag":830,"props":2305,"children":2306},{"style":1258},[2307],{"type":349,"value":2308},"\"packages/*\"\n",{"type":339,"tag":830,"props":2310,"children":2311},{"class":832,"line":140},[2312],{"type":339,"tag":830,"props":2313,"children":2314},{"style":2266},[2315],{"type":349,"value":2316},"  # API lambda functions are workspace packages too, so they can import shared packages\n",{"type":339,"tag":830,"props":2318,"children":2319},{"class":832,"line":162},[2320,2324],{"type":339,"tag":830,"props":2321,"children":2322},{"style":1222},[2323],{"type":349,"value":2291},{"type":339,"tag":830,"props":2325,"children":2326},{"style":1258},[2327],{"type":349,"value":2328},"\"apps/web/functions/*\"\n",{"type":339,"tag":750,"props":2330,"children":2332},{"id":2331},"idea-1-the-frontend-reads-terraform-outputs",[2333],{"type":349,"value":2334},"Idea 1: the frontend reads Terraform outputs",{"type":339,"tag":345,"props":2336,"children":2337},{},[2338,2340,2346],{"type":349,"value":2339},"Amplify is in this stack, but only as the client library. It configures Cognito and attaches the bearer token to REST calls. There is no Amplify backend, no Amplify hosting, no ",{"type":339,"tag":398,"props":2341,"children":2343},{"className":2342},[],[2344],{"type":349,"value":2345},"amplify/",{"type":349,"value":2347}," directory. The config it needs is generated from Terraform.",{"type":339,"tag":345,"props":2349,"children":2350},{},[2351,2353,2359],{"type":349,"value":2352},"The trick is a naming convention on outputs. Anything prefixed ",{"type":339,"tag":398,"props":2354,"children":2356},{"className":2355},[],[2357],{"type":349,"value":2358},"Amplify_",{"type":349,"value":2360}," is folded into a nested object by splitting on underscores:",{"type":339,"tag":820,"props":2362,"children":2366},{"className":2363,"code":2364,"language":2365,"meta":8,"style":8},"language-hcl shiki shiki-themes github-dark","output \"Amplify_Auth_Cognito_userPoolId\" {\n  value = module.auth.user_pool_id\n}\n\noutput \"Amplify_API_REST_main-api_endpoint\" {\n  value = module.api.invoke_url\n}\n","hcl",[2367],{"type":339,"tag":398,"props":2368,"children":2369},{"__ignoreMap":8},[2370,2378,2386,2393,2400,2408,2416],{"type":339,"tag":830,"props":2371,"children":2372},{"class":832,"line":108},[2373],{"type":339,"tag":830,"props":2374,"children":2375},{},[2376],{"type":349,"value":2377},"output \"Amplify_Auth_Cognito_userPoolId\" {\n",{"type":339,"tag":830,"props":2379,"children":2380},{"class":832,"line":129},[2381],{"type":339,"tag":830,"props":2382,"children":2383},{},[2384],{"type":349,"value":2385},"  value = module.auth.user_pool_id\n",{"type":339,"tag":830,"props":2387,"children":2388},{"class":832,"line":151},[2389],{"type":339,"tag":830,"props":2390,"children":2391},{},[2392],{"type":349,"value":1168},{"type":339,"tag":830,"props":2394,"children":2395},{"class":832,"line":173},[2396],{"type":339,"tag":830,"props":2397,"children":2398},{"emptyLinePlaceholder":641},[2399],{"type":349,"value":1794},{"type":339,"tag":830,"props":2401,"children":2402},{"class":832,"line":140},[2403],{"type":339,"tag":830,"props":2404,"children":2405},{},[2406],{"type":349,"value":2407},"output \"Amplify_API_REST_main-api_endpoint\" {\n",{"type":339,"tag":830,"props":2409,"children":2410},{"class":832,"line":162},[2411],{"type":339,"tag":830,"props":2412,"children":2413},{},[2414],{"type":349,"value":2415},"  value = module.api.invoke_url\n",{"type":339,"tag":830,"props":2417,"children":2418},{"class":832,"line":184},[2419],{"type":339,"tag":830,"props":2420,"children":2421},{},[2422],{"type":349,"value":1168},{"type":339,"tag":820,"props":2424,"children":2426},{"className":1066,"code":2425,"language":1068,"meta":8,"style":8},"def parse_terraform_output(output_json):\n    \"\"\"Fold 'terraform output -json' into the nested shape Amplify.configure wants.\"\"\"\n    amplify_config = {}\n    for key, payload in output_json.items():\n        if not key.startswith(\"Amplify\"):\n            continue\n        parts = key.split(\"_\")[1:]          # drop the Amplify prefix\n        level = amplify_config\n        for part in parts[:-1]:\n            level = level.setdefault(part, {})\n        level[parts[-1]] = payload[\"value\"]\n    return amplify_config\n",[2427],{"type":339,"tag":398,"props":2428,"children":2429},{"__ignoreMap":8},[2430,2438,2446,2454,2462,2470,2478,2486,2494,2502,2510,2518],{"type":339,"tag":830,"props":2431,"children":2432},{"class":832,"line":108},[2433],{"type":339,"tag":830,"props":2434,"children":2435},{},[2436],{"type":349,"value":2437},"def parse_terraform_output(output_json):\n",{"type":339,"tag":830,"props":2439,"children":2440},{"class":832,"line":129},[2441],{"type":339,"tag":830,"props":2442,"children":2443},{},[2444],{"type":349,"value":2445},"    \"\"\"Fold 'terraform output -json' into the nested shape Amplify.configure wants.\"\"\"\n",{"type":339,"tag":830,"props":2447,"children":2448},{"class":832,"line":151},[2449],{"type":339,"tag":830,"props":2450,"children":2451},{},[2452],{"type":349,"value":2453},"    amplify_config = {}\n",{"type":339,"tag":830,"props":2455,"children":2456},{"class":832,"line":173},[2457],{"type":339,"tag":830,"props":2458,"children":2459},{},[2460],{"type":349,"value":2461},"    for key, payload in output_json.items():\n",{"type":339,"tag":830,"props":2463,"children":2464},{"class":832,"line":140},[2465],{"type":339,"tag":830,"props":2466,"children":2467},{},[2468],{"type":349,"value":2469},"        if not key.startswith(\"Amplify\"):\n",{"type":339,"tag":830,"props":2471,"children":2472},{"class":832,"line":162},[2473],{"type":339,"tag":830,"props":2474,"children":2475},{},[2476],{"type":349,"value":2477},"            continue\n",{"type":339,"tag":830,"props":2479,"children":2480},{"class":832,"line":184},[2481],{"type":339,"tag":830,"props":2482,"children":2483},{},[2484],{"type":349,"value":2485},"        parts = key.split(\"_\")[1:]          # drop the Amplify prefix\n",{"type":339,"tag":830,"props":2487,"children":2488},{"class":832,"line":49},[2489],{"type":339,"tag":830,"props":2490,"children":2491},{},[2492],{"type":349,"value":2493},"        level = amplify_config\n",{"type":339,"tag":830,"props":2495,"children":2496},{"class":832,"line":70},[2497],{"type":339,"tag":830,"props":2498,"children":2499},{},[2500],{"type":349,"value":2501},"        for part in parts[:-1]:\n",{"type":339,"tag":830,"props":2503,"children":2504},{"class":832,"line":60},[2505],{"type":339,"tag":830,"props":2506,"children":2507},{},[2508],{"type":349,"value":2509},"            level = level.setdefault(part, {})\n",{"type":339,"tag":830,"props":2511,"children":2512},{"class":832,"line":81},[2513],{"type":339,"tag":830,"props":2514,"children":2515},{},[2516],{"type":349,"value":2517},"        level[parts[-1]] = payload[\"value\"]\n",{"type":339,"tag":830,"props":2519,"children":2520},{"class":832,"line":90},[2521],{"type":339,"tag":830,"props":2522,"children":2523},{},[2524],{"type":349,"value":2525},"    return amplify_config\n",{"type":339,"tag":345,"props":2527,"children":2528},{},[2529,2531,2537,2539,2545,2547,2553,2555,2561,2563,2569,2571,2577,2579,2585],{"type":349,"value":2530},"That writes ",{"type":339,"tag":398,"props":2532,"children":2534},{"className":2533},[],[2535],{"type":349,"value":2536},"packages/auth/src/amplify-config.json",{"type":349,"value":2538},", which the auth provider imports and hands to ",{"type":339,"tag":398,"props":2540,"children":2542},{"className":2541},[],[2543],{"type":349,"value":2544},"Amplify.configure()",{"type":349,"value":2546},". Running ",{"type":339,"tag":398,"props":2548,"children":2550},{"className":2549},[],[2551],{"type":349,"value":2552},"pnpm checkout --env dev",{"type":349,"value":2554}," re-inits Terraform against the dev state bucket, runs ",{"type":339,"tag":398,"props":2556,"children":2558},{"className":2557},[],[2559],{"type":349,"value":2560},"terraform output -json",{"type":349,"value":2562},", regenerates the file and sets the environment name in ",{"type":339,"tag":398,"props":2564,"children":2566},{"className":2565},[],[2567],{"type":349,"value":2568},".env.local",{"type":349,"value":2570},". Point the same command at ",{"type":339,"tag":398,"props":2572,"children":2574},{"className":2573},[],[2575],{"type":349,"value":2576},"test",{"type":349,"value":2578},", ",{"type":339,"tag":398,"props":2580,"children":2582},{"className":2581},[],[2583],{"type":349,"value":2584},"prod",{"type":349,"value":2586}," or your sandbox and the frontend follows.",{"type":339,"tag":345,"props":2588,"children":2589},{},[2590,2592,2598],{"type":349,"value":2591},"The state bucket per environment lives in a plain ",{"type":339,"tag":398,"props":2593,"children":2595},{"className":2594},[],[2596],{"type":349,"value":2597},"config.ini",{"type":349,"value":2599}," at the repo root. Two keys per section, region and bucket name, no secrets. Every script reads it. It is the least clever file in the repo and the one I would keep in any rewrite.",{"type":339,"tag":750,"props":2601,"children":2603},{"id":2602},"idea-2-lambdas-are-workspace-packages-and-terraform-builds-them",[2604],{"type":349,"value":2605},"Idea 2: Lambdas are workspace packages and Terraform builds them",{"type":339,"tag":345,"props":2607,"children":2608},{},[2609,2611,2617,2619,2625],{"type":349,"value":2610},"Each function under ",{"type":339,"tag":398,"props":2612,"children":2614},{"className":2613},[],[2615],{"type":349,"value":2616},"apps/web/functions/",{"type":349,"value":2618}," has its own ",{"type":339,"tag":398,"props":2620,"children":2622},{"className":2621},[],[2623],{"type":349,"value":2624},"package.json",{"type":349,"value":2626}," and is a pnpm workspace member. That gives it real dependency management and, more importantly, lets it import the shared packages: the API contracts, the tsconfig, an esbuild wrapper.",{"type":339,"tag":345,"props":2628,"children":2629},{},[2630,2632,2638],{"type":349,"value":2631},"Terraform packages it using the community Lambda module's ",{"type":339,"tag":398,"props":2633,"children":2635},{"className":2634},[],[2636],{"type":349,"value":2637},"source_path.commands",{"type":349,"value":2639},". The commands install the workspace, build with esbuild and zip the output. The odd-looking part is the lock directory:",{"type":339,"tag":820,"props":2641,"children":2643},{"className":2363,"code":2642,"language":2365,"meta":8,"style":8},"source_path = [{\n  path = \"${path.root}/../apps/web/functions/${var.slug}\"\n  commands = [\n    \"set -e\",\n    \"LOCK=../../../../.pnpm-install.lock\",\n    \"until mkdir \\\"$LOCK\\\" 2>/dev/null; do sleep 1; done; trap 'rmdir \\\"$LOCK\\\"' EXIT\",\n    \"pnpm install --frozen-lockfile\",\n    \"rmdir \\\"$LOCK\\\"; trap - EXIT\",\n    \"pnpm run build\",\n    \":zip dist\",\n  ]\n  patterns = [\"!node_modules/.*\", \"!dist/.*\", \"!.*\\\\.js$\"]\n}]\n\nhash_extra = local.shared_packages_hash\n",[2644],{"type":339,"tag":398,"props":2645,"children":2646},{"__ignoreMap":8},[2647,2655,2663,2671,2679,2687,2695,2703,2711,2719,2727,2735,2743,2751,2758],{"type":339,"tag":830,"props":2648,"children":2649},{"class":832,"line":108},[2650],{"type":339,"tag":830,"props":2651,"children":2652},{},[2653],{"type":349,"value":2654},"source_path = [{\n",{"type":339,"tag":830,"props":2656,"children":2657},{"class":832,"line":129},[2658],{"type":339,"tag":830,"props":2659,"children":2660},{},[2661],{"type":349,"value":2662},"  path = \"${path.root}/../apps/web/functions/${var.slug}\"\n",{"type":339,"tag":830,"props":2664,"children":2665},{"class":832,"line":151},[2666],{"type":339,"tag":830,"props":2667,"children":2668},{},[2669],{"type":349,"value":2670},"  commands = [\n",{"type":339,"tag":830,"props":2672,"children":2673},{"class":832,"line":173},[2674],{"type":339,"tag":830,"props":2675,"children":2676},{},[2677],{"type":349,"value":2678},"    \"set -e\",\n",{"type":339,"tag":830,"props":2680,"children":2681},{"class":832,"line":140},[2682],{"type":339,"tag":830,"props":2683,"children":2684},{},[2685],{"type":349,"value":2686},"    \"LOCK=../../../../.pnpm-install.lock\",\n",{"type":339,"tag":830,"props":2688,"children":2689},{"class":832,"line":162},[2690],{"type":339,"tag":830,"props":2691,"children":2692},{},[2693],{"type":349,"value":2694},"    \"until mkdir \\\"$LOCK\\\" 2>/dev/null; do sleep 1; done; trap 'rmdir \\\"$LOCK\\\"' EXIT\",\n",{"type":339,"tag":830,"props":2696,"children":2697},{"class":832,"line":184},[2698],{"type":339,"tag":830,"props":2699,"children":2700},{},[2701],{"type":349,"value":2702},"    \"pnpm install --frozen-lockfile\",\n",{"type":339,"tag":830,"props":2704,"children":2705},{"class":832,"line":49},[2706],{"type":339,"tag":830,"props":2707,"children":2708},{},[2709],{"type":349,"value":2710},"    \"rmdir \\\"$LOCK\\\"; trap - EXIT\",\n",{"type":339,"tag":830,"props":2712,"children":2713},{"class":832,"line":70},[2714],{"type":339,"tag":830,"props":2715,"children":2716},{},[2717],{"type":349,"value":2718},"    \"pnpm run build\",\n",{"type":339,"tag":830,"props":2720,"children":2721},{"class":832,"line":60},[2722],{"type":339,"tag":830,"props":2723,"children":2724},{},[2725],{"type":349,"value":2726},"    \":zip dist\",\n",{"type":339,"tag":830,"props":2728,"children":2729},{"class":832,"line":81},[2730],{"type":339,"tag":830,"props":2731,"children":2732},{},[2733],{"type":349,"value":2734},"  ]\n",{"type":339,"tag":830,"props":2736,"children":2737},{"class":832,"line":90},[2738],{"type":339,"tag":830,"props":2739,"children":2740},{},[2741],{"type":349,"value":2742},"  patterns = [\"!node_modules/.*\", \"!dist/.*\", \"!.*\\\\.js$\"]\n",{"type":339,"tag":830,"props":2744,"children":2745},{"class":832,"line":15},[2746],{"type":339,"tag":830,"props":2747,"children":2748},{},[2749],{"type":349,"value":2750},"}]\n",{"type":339,"tag":830,"props":2752,"children":2753},{"class":832,"line":28},[2754],{"type":339,"tag":830,"props":2755,"children":2756},{"emptyLinePlaceholder":641},[2757],{"type":349,"value":1794},{"type":339,"tag":830,"props":2759,"children":2760},{"class":832,"line":221},[2761],{"type":339,"tag":830,"props":2762,"children":2763},{},[2764],{"type":349,"value":2765},"hash_extra = local.shared_packages_hash\n",{"type":339,"tag":345,"props":2767,"children":2768},{},[2769,2771,2777,2779,2785],{"type":349,"value":2770},"Terraform builds every function's package in parallel, and parallel ",{"type":339,"tag":398,"props":2772,"children":2774},{"className":2773},[],[2775],{"type":349,"value":2776},"pnpm install",{"type":349,"value":2778}," calls in one workspace corrupt each other, so the first one to ",{"type":339,"tag":398,"props":2780,"children":2782},{"className":2781},[],[2783],{"type":349,"value":2784},"mkdir",{"type":349,"value":2786}," the lock wins and the rest wait. It is a one-line mutex and it has never failed.",{"type":339,"tag":345,"props":2788,"children":2789},{},[2790,2791,2797,2799,2805,2807,2812,2814,2820],{"type":349,"value":1668},{"type":339,"tag":398,"props":2792,"children":2794},{"className":2793},[],[2795],{"type":349,"value":2796},"hash_extra",{"type":349,"value":2798}," line is the part that took me longest to get right. The module decides whether to rebuild a function by hashing its source, and the source patterns exclude ",{"type":339,"tag":398,"props":2800,"children":2802},{"className":2801},[],[2803],{"type":349,"value":2804},"node_modules",{"type":349,"value":2806},". So a change to a shared package would never redeploy the functions that import it. ",{"type":339,"tag":398,"props":2808,"children":2810},{"className":2809},[],[2811],{"type":349,"value":2796},{"type":349,"value":2813}," is a hash of every file under ",{"type":339,"tag":398,"props":2815,"children":2817},{"className":2816},[],[2818],{"type":349,"value":2819},"packages/*/src",{"type":349,"value":2821}," plus the lockfile. Change the contracts package and every function redeploys. Change one handler and only that function does.",{"type":339,"tag":750,"props":2823,"children":2825},{"id":2824},"idea-3-one-contract-three-consumers",[2826],{"type":349,"value":2827},"Idea 3: one contract, three consumers",{"type":339,"tag":345,"props":2829,"children":2830},{},[2831,2833,2839],{"type":349,"value":2832},"The API is defined once, as zod schemas in ",{"type":339,"tag":398,"props":2834,"children":2836},{"className":2835},[],[2837],{"type":349,"value":2838},"packages/api-contracts",{"type":349,"value":2840},":",{"type":339,"tag":820,"props":2842,"children":2846},{"className":2843,"code":2844,"language":2845,"meta":8,"style":8},"language-typescript shiki shiki-themes github-dark","export const todos = group(\"/todos\", {\n  list:   get(\"/\",     { response: z.array(Todo) }),\n  create: post(\"/\",    { body: TodoInput, response: Todo }),\n  update: patch(\"/:id\", { params: z.object({ id: z.string() }), body: TodoInput.partial(), response: Todo }),\n  remove: del(\"/:id\",  { params: z.object({ id: z.string() }) }),\n});\n","typescript",[2847],{"type":339,"tag":398,"props":2848,"children":2849},{"__ignoreMap":8},[2850,2895,2932,2958,3015,3058],{"type":339,"tag":830,"props":2851,"children":2852},{"class":832,"line":108},[2853,2859,2864,2869,2874,2880,2885,2890],{"type":339,"tag":830,"props":2854,"children":2856},{"style":2855},"--shiki-default:#F97583",[2857],{"type":349,"value":2858},"export",{"type":339,"tag":830,"props":2860,"children":2861},{"style":2855},[2862],{"type":349,"value":2863}," const",{"type":339,"tag":830,"props":2865,"children":2866},{"style":1231},[2867],{"type":349,"value":2868}," todos",{"type":339,"tag":830,"props":2870,"children":2871},{"style":2855},[2872],{"type":349,"value":2873}," =",{"type":339,"tag":830,"props":2875,"children":2877},{"style":2876},"--shiki-default:#B392F0",[2878],{"type":349,"value":2879}," group",{"type":339,"tag":830,"props":2881,"children":2882},{"style":1222},[2883],{"type":349,"value":2884},"(",{"type":339,"tag":830,"props":2886,"children":2887},{"style":1258},[2888],{"type":349,"value":2889},"\"/todos\"",{"type":339,"tag":830,"props":2891,"children":2892},{"style":1222},[2893],{"type":349,"value":2894},", {\n",{"type":339,"tag":830,"props":2896,"children":2897},{"class":832,"line":129},[2898,2903,2908,2912,2917,2922,2927],{"type":339,"tag":830,"props":2899,"children":2900},{"style":1222},[2901],{"type":349,"value":2902},"  list:   ",{"type":339,"tag":830,"props":2904,"children":2905},{"style":2876},[2906],{"type":349,"value":2907},"get",{"type":339,"tag":830,"props":2909,"children":2910},{"style":1222},[2911],{"type":349,"value":2884},{"type":339,"tag":830,"props":2913,"children":2914},{"style":1258},[2915],{"type":349,"value":2916},"\"/\"",{"type":339,"tag":830,"props":2918,"children":2919},{"style":1222},[2920],{"type":349,"value":2921},",     { response: z.",{"type":339,"tag":830,"props":2923,"children":2924},{"style":2876},[2925],{"type":349,"value":2926},"array",{"type":339,"tag":830,"props":2928,"children":2929},{"style":1222},[2930],{"type":349,"value":2931},"(Todo) }),\n",{"type":339,"tag":830,"props":2933,"children":2934},{"class":832,"line":151},[2935,2940,2945,2949,2953],{"type":339,"tag":830,"props":2936,"children":2937},{"style":1222},[2938],{"type":349,"value":2939},"  create: ",{"type":339,"tag":830,"props":2941,"children":2942},{"style":2876},[2943],{"type":349,"value":2944},"post",{"type":339,"tag":830,"props":2946,"children":2947},{"style":1222},[2948],{"type":349,"value":2884},{"type":339,"tag":830,"props":2950,"children":2951},{"style":1258},[2952],{"type":349,"value":2916},{"type":339,"tag":830,"props":2954,"children":2955},{"style":1222},[2956],{"type":349,"value":2957},",    { body: TodoInput, response: Todo }),\n",{"type":339,"tag":830,"props":2959,"children":2960},{"class":832,"line":173},[2961,2966,2971,2975,2980,2985,2990,2995,3000,3005,3010],{"type":339,"tag":830,"props":2962,"children":2963},{"style":1222},[2964],{"type":349,"value":2965},"  update: ",{"type":339,"tag":830,"props":2967,"children":2968},{"style":2876},[2969],{"type":349,"value":2970},"patch",{"type":339,"tag":830,"props":2972,"children":2973},{"style":1222},[2974],{"type":349,"value":2884},{"type":339,"tag":830,"props":2976,"children":2977},{"style":1258},[2978],{"type":349,"value":2979},"\"/:id\"",{"type":339,"tag":830,"props":2981,"children":2982},{"style":1222},[2983],{"type":349,"value":2984},", { params: z.",{"type":339,"tag":830,"props":2986,"children":2987},{"style":2876},[2988],{"type":349,"value":2989},"object",{"type":339,"tag":830,"props":2991,"children":2992},{"style":1222},[2993],{"type":349,"value":2994},"({ id: z.",{"type":339,"tag":830,"props":2996,"children":2997},{"style":2876},[2998],{"type":349,"value":2999},"string",{"type":339,"tag":830,"props":3001,"children":3002},{"style":1222},[3003],{"type":349,"value":3004},"() }), body: TodoInput.",{"type":339,"tag":830,"props":3006,"children":3007},{"style":2876},[3008],{"type":349,"value":3009},"partial",{"type":339,"tag":830,"props":3011,"children":3012},{"style":1222},[3013],{"type":349,"value":3014},"(), response: Todo }),\n",{"type":339,"tag":830,"props":3016,"children":3017},{"class":832,"line":140},[3018,3023,3028,3032,3036,3041,3045,3049,3053],{"type":339,"tag":830,"props":3019,"children":3020},{"style":1222},[3021],{"type":349,"value":3022},"  remove: ",{"type":339,"tag":830,"props":3024,"children":3025},{"style":2876},[3026],{"type":349,"value":3027},"del",{"type":339,"tag":830,"props":3029,"children":3030},{"style":1222},[3031],{"type":349,"value":2884},{"type":339,"tag":830,"props":3033,"children":3034},{"style":1258},[3035],{"type":349,"value":2979},{"type":339,"tag":830,"props":3037,"children":3038},{"style":1222},[3039],{"type":349,"value":3040},",  { params: z.",{"type":339,"tag":830,"props":3042,"children":3043},{"style":2876},[3044],{"type":349,"value":2989},{"type":339,"tag":830,"props":3046,"children":3047},{"style":1222},[3048],{"type":349,"value":2994},{"type":339,"tag":830,"props":3050,"children":3051},{"style":2876},[3052],{"type":349,"value":2999},{"type":339,"tag":830,"props":3054,"children":3055},{"style":1222},[3056],{"type":349,"value":3057},"() }) }),\n",{"type":339,"tag":830,"props":3059,"children":3060},{"class":832,"line":162},[3061],{"type":339,"tag":830,"props":3062,"children":3063},{"style":1222},[3064],{"type":349,"value":3065},"});\n",{"type":339,"tag":345,"props":3067,"children":3068},{},[3069],{"type":349,"value":3070},"Three things are generated or derived from that one file, and none of them can drift from the others.",{"type":339,"tag":345,"props":3072,"children":3073},{},[3074],{"type":339,"tag":784,"props":3075,"children":3078},{"alt":3076,"src":3077},"One zod route contract fans out to three consumers: an API manifest JSON that Terraform reads to create gateway resources and methods, a typed client used by React Query hooks in the SPA, and a Lambda router that validates the request against the same schema","/articles/serverless-toolkit-contract.svg",[],{"type":339,"tag":345,"props":3080,"children":3081},{},[3082,3084,3090],{"type":349,"value":3083},"The first consumer is Terraform. A pre-commit hook writes ",{"type":339,"tag":398,"props":3085,"children":3087},{"className":3086},[],[3088],{"type":349,"value":3089},"terraform/api-manifest.json",{"type":349,"value":3091}," from the contracts, and each endpoint module reads its routes from it. The authorisation rules stay in HCL, because they are infrastructure:",{"type":339,"tag":820,"props":3093,"children":3095},{"className":2363,"code":3094,"language":2365,"meta":8,"style":8},"module \"todos\" {\n  source = \"../modules/endpoint\"\n\n  slug   = \"todos\"\n  path   = \"/todos\"\n  routes = local.manifest.endpoints[\"/todos\"].routes\n\n  methods = {\n    GET    = { allow_unauthenticated = true }\n    POST   = { allowed_groups = [\"User\", \"Admin\"] }\n    PATCH  = { allowed_groups = [\"User\", \"Admin\"] }\n    DELETE = { allowed_groups = [\"Admin\"] }\n  }\n\n  dynamo_tables = {\n    DDB_TABLE_NAME = { arn = var.dynamodb_table.arn, name = var.dynamodb_table.name }\n  }\n}\n",[3096],{"type":339,"tag":398,"props":3097,"children":3098},{"__ignoreMap":8},[3099,3107,3115,3122,3130,3138,3146,3153,3161,3169,3177,3185,3193,3200,3207,3215,3223,3230],{"type":339,"tag":830,"props":3100,"children":3101},{"class":832,"line":108},[3102],{"type":339,"tag":830,"props":3103,"children":3104},{},[3105],{"type":349,"value":3106},"module \"todos\" {\n",{"type":339,"tag":830,"props":3108,"children":3109},{"class":832,"line":129},[3110],{"type":339,"tag":830,"props":3111,"children":3112},{},[3113],{"type":349,"value":3114},"  source = \"../modules/endpoint\"\n",{"type":339,"tag":830,"props":3116,"children":3117},{"class":832,"line":151},[3118],{"type":339,"tag":830,"props":3119,"children":3120},{"emptyLinePlaceholder":641},[3121],{"type":349,"value":1794},{"type":339,"tag":830,"props":3123,"children":3124},{"class":832,"line":173},[3125],{"type":339,"tag":830,"props":3126,"children":3127},{},[3128],{"type":349,"value":3129},"  slug   = \"todos\"\n",{"type":339,"tag":830,"props":3131,"children":3132},{"class":832,"line":140},[3133],{"type":339,"tag":830,"props":3134,"children":3135},{},[3136],{"type":349,"value":3137},"  path   = \"/todos\"\n",{"type":339,"tag":830,"props":3139,"children":3140},{"class":832,"line":162},[3141],{"type":339,"tag":830,"props":3142,"children":3143},{},[3144],{"type":349,"value":3145},"  routes = local.manifest.endpoints[\"/todos\"].routes\n",{"type":339,"tag":830,"props":3147,"children":3148},{"class":832,"line":184},[3149],{"type":339,"tag":830,"props":3150,"children":3151},{"emptyLinePlaceholder":641},[3152],{"type":349,"value":1794},{"type":339,"tag":830,"props":3154,"children":3155},{"class":832,"line":49},[3156],{"type":339,"tag":830,"props":3157,"children":3158},{},[3159],{"type":349,"value":3160},"  methods = {\n",{"type":339,"tag":830,"props":3162,"children":3163},{"class":832,"line":70},[3164],{"type":339,"tag":830,"props":3165,"children":3166},{},[3167],{"type":349,"value":3168},"    GET    = { allow_unauthenticated = true }\n",{"type":339,"tag":830,"props":3170,"children":3171},{"class":832,"line":60},[3172],{"type":339,"tag":830,"props":3173,"children":3174},{},[3175],{"type":349,"value":3176},"    POST   = { allowed_groups = [\"User\", \"Admin\"] }\n",{"type":339,"tag":830,"props":3178,"children":3179},{"class":832,"line":81},[3180],{"type":339,"tag":830,"props":3181,"children":3182},{},[3183],{"type":349,"value":3184},"    PATCH  = { allowed_groups = [\"User\", \"Admin\"] }\n",{"type":339,"tag":830,"props":3186,"children":3187},{"class":832,"line":90},[3188],{"type":339,"tag":830,"props":3189,"children":3190},{},[3191],{"type":349,"value":3192},"    DELETE = { allowed_groups = [\"Admin\"] }\n",{"type":339,"tag":830,"props":3194,"children":3195},{"class":832,"line":15},[3196],{"type":339,"tag":830,"props":3197,"children":3198},{},[3199],{"type":349,"value":1650},{"type":339,"tag":830,"props":3201,"children":3202},{"class":832,"line":28},[3203],{"type":339,"tag":830,"props":3204,"children":3205},{"emptyLinePlaceholder":641},[3206],{"type":349,"value":1794},{"type":339,"tag":830,"props":3208,"children":3209},{"class":832,"line":221},[3210],{"type":339,"tag":830,"props":3211,"children":3212},{},[3213],{"type":349,"value":3214},"  dynamo_tables = {\n",{"type":339,"tag":830,"props":3216,"children":3217},{"class":832,"line":232},[3218],{"type":339,"tag":830,"props":3219,"children":3220},{},[3221],{"type":349,"value":3222},"    DDB_TABLE_NAME = { arn = var.dynamodb_table.arn, name = var.dynamodb_table.name }\n",{"type":339,"tag":830,"props":3224,"children":3225},{"class":832,"line":196},[3226],{"type":339,"tag":830,"props":3227,"children":3228},{},[3229],{"type":349,"value":1650},{"type":339,"tag":830,"props":3231,"children":3232},{"class":832,"line":209},[3233],{"type":339,"tag":830,"props":3234,"children":3235},{},[3236],{"type":349,"value":1168},{"type":339,"tag":345,"props":3238,"children":3239},{},[3240,3242,3248],{"type":349,"value":3241},"The second consumer is the frontend, which gets a typed client and uses it inside React Query hooks. The third is the handler itself, which is wired through a router that validates params, query and body against the same schema before your code runs, and turns thrown ",{"type":339,"tag":398,"props":3243,"children":3245},{"className":3244},[],[3246],{"type":349,"value":3247},"HttpError",{"type":349,"value":3249},"s into RFC 9457 problem details:",{"type":339,"tag":820,"props":3251,"children":3253},{"className":2843,"code":3252,"language":2845,"meta":8,"style":8},"export const lambda_handler = router(todos, {\n  list: async () => response(200, await listTodos()),\n  create: async ({ body }) => {\n    const todo: Todo = { id: randomUUID(), ...body, done: false };\n    await dynamodb.send(new PutCommand({ TableName: tableName, Item: todo }));\n    metrics.addMetric(\"TodosCreated\", MetricUnit.Count, 1);\n    return response(201, todo);\n  },\n}, { logger, tracer, metrics });\n",[3254],{"type":339,"tag":398,"props":3255,"children":3256},{"__ignoreMap":8},[3257,3287,3347,3388,3448,3485,3521,3547,3554],{"type":339,"tag":830,"props":3258,"children":3259},{"class":832,"line":108},[3260,3264,3268,3273,3277,3282],{"type":339,"tag":830,"props":3261,"children":3262},{"style":2855},[3263],{"type":349,"value":2858},{"type":339,"tag":830,"props":3265,"children":3266},{"style":2855},[3267],{"type":349,"value":2863},{"type":339,"tag":830,"props":3269,"children":3270},{"style":1231},[3271],{"type":349,"value":3272}," lambda_handler",{"type":339,"tag":830,"props":3274,"children":3275},{"style":2855},[3276],{"type":349,"value":2873},{"type":339,"tag":830,"props":3278,"children":3279},{"style":2876},[3280],{"type":349,"value":3281}," router",{"type":339,"tag":830,"props":3283,"children":3284},{"style":1222},[3285],{"type":349,"value":3286},"(todos, {\n",{"type":339,"tag":830,"props":3288,"children":3289},{"class":832,"line":129},[3290,3295,3299,3304,3309,3314,3319,3323,3328,3332,3337,3342],{"type":339,"tag":830,"props":3291,"children":3292},{"style":2876},[3293],{"type":349,"value":3294},"  list",{"type":339,"tag":830,"props":3296,"children":3297},{"style":1222},[3298],{"type":349,"value":1371},{"type":339,"tag":830,"props":3300,"children":3301},{"style":2855},[3302],{"type":349,"value":3303},"async",{"type":339,"tag":830,"props":3305,"children":3306},{"style":1222},[3307],{"type":349,"value":3308}," () ",{"type":339,"tag":830,"props":3310,"children":3311},{"style":2855},[3312],{"type":349,"value":3313},"=>",{"type":339,"tag":830,"props":3315,"children":3316},{"style":2876},[3317],{"type":349,"value":3318}," response",{"type":339,"tag":830,"props":3320,"children":3321},{"style":1222},[3322],{"type":349,"value":2884},{"type":339,"tag":830,"props":3324,"children":3325},{"style":1231},[3326],{"type":349,"value":3327},"200",{"type":339,"tag":830,"props":3329,"children":3330},{"style":1222},[3331],{"type":349,"value":2578},{"type":339,"tag":830,"props":3333,"children":3334},{"style":2855},[3335],{"type":349,"value":3336},"await",{"type":339,"tag":830,"props":3338,"children":3339},{"style":2876},[3340],{"type":349,"value":3341}," listTodos",{"type":339,"tag":830,"props":3343,"children":3344},{"style":1222},[3345],{"type":349,"value":3346},"()),\n",{"type":339,"tag":830,"props":3348,"children":3349},{"class":832,"line":151},[3350,3355,3359,3363,3368,3374,3379,3383],{"type":339,"tag":830,"props":3351,"children":3352},{"style":2876},[3353],{"type":349,"value":3354},"  create",{"type":339,"tag":830,"props":3356,"children":3357},{"style":1222},[3358],{"type":349,"value":1371},{"type":339,"tag":830,"props":3360,"children":3361},{"style":2855},[3362],{"type":349,"value":3303},{"type":339,"tag":830,"props":3364,"children":3365},{"style":1222},[3366],{"type":349,"value":3367}," ({ ",{"type":339,"tag":830,"props":3369,"children":3371},{"style":3370},"--shiki-default:#FFAB70",[3372],{"type":349,"value":3373},"body",{"type":339,"tag":830,"props":3375,"children":3376},{"style":1222},[3377],{"type":349,"value":3378}," }) ",{"type":339,"tag":830,"props":3380,"children":3381},{"style":2855},[3382],{"type":349,"value":3313},{"type":339,"tag":830,"props":3384,"children":3385},{"style":1222},[3386],{"type":349,"value":3387}," {\n",{"type":339,"tag":830,"props":3389,"children":3390},{"class":832,"line":173},[3391,3396,3401,3405,3410,3414,3419,3424,3429,3434,3439,3443],{"type":339,"tag":830,"props":3392,"children":3393},{"style":2855},[3394],{"type":349,"value":3395},"    const",{"type":339,"tag":830,"props":3397,"children":3398},{"style":1231},[3399],{"type":349,"value":3400}," todo",{"type":339,"tag":830,"props":3402,"children":3403},{"style":2855},[3404],{"type":349,"value":2840},{"type":339,"tag":830,"props":3406,"children":3407},{"style":2876},[3408],{"type":349,"value":3409}," Todo",{"type":339,"tag":830,"props":3411,"children":3412},{"style":2855},[3413],{"type":349,"value":2873},{"type":339,"tag":830,"props":3415,"children":3416},{"style":1222},[3417],{"type":349,"value":3418}," { id: ",{"type":339,"tag":830,"props":3420,"children":3421},{"style":2876},[3422],{"type":349,"value":3423},"randomUUID",{"type":339,"tag":830,"props":3425,"children":3426},{"style":1222},[3427],{"type":349,"value":3428},"(), ",{"type":339,"tag":830,"props":3430,"children":3431},{"style":2855},[3432],{"type":349,"value":3433},"...",{"type":339,"tag":830,"props":3435,"children":3436},{"style":1222},[3437],{"type":349,"value":3438},"body, done: ",{"type":339,"tag":830,"props":3440,"children":3441},{"style":1231},[3442],{"type":349,"value":1532},{"type":339,"tag":830,"props":3444,"children":3445},{"style":1222},[3446],{"type":349,"value":3447}," };\n",{"type":339,"tag":830,"props":3449,"children":3450},{"class":832,"line":140},[3451,3456,3461,3466,3470,3475,3480],{"type":339,"tag":830,"props":3452,"children":3453},{"style":2855},[3454],{"type":349,"value":3455},"    await",{"type":339,"tag":830,"props":3457,"children":3458},{"style":1222},[3459],{"type":349,"value":3460}," dynamodb.",{"type":339,"tag":830,"props":3462,"children":3463},{"style":2876},[3464],{"type":349,"value":3465},"send",{"type":339,"tag":830,"props":3467,"children":3468},{"style":1222},[3469],{"type":349,"value":2884},{"type":339,"tag":830,"props":3471,"children":3472},{"style":2855},[3473],{"type":349,"value":3474},"new",{"type":339,"tag":830,"props":3476,"children":3477},{"style":2876},[3478],{"type":349,"value":3479}," PutCommand",{"type":339,"tag":830,"props":3481,"children":3482},{"style":1222},[3483],{"type":349,"value":3484},"({ TableName: tableName, Item: todo }));\n",{"type":339,"tag":830,"props":3486,"children":3487},{"class":832,"line":162},[3488,3493,3498,3502,3507,3512,3516],{"type":339,"tag":830,"props":3489,"children":3490},{"style":1222},[3491],{"type":349,"value":3492},"    metrics.",{"type":339,"tag":830,"props":3494,"children":3495},{"style":2876},[3496],{"type":349,"value":3497},"addMetric",{"type":339,"tag":830,"props":3499,"children":3500},{"style":1222},[3501],{"type":349,"value":2884},{"type":339,"tag":830,"props":3503,"children":3504},{"style":1258},[3505],{"type":349,"value":3506},"\"TodosCreated\"",{"type":339,"tag":830,"props":3508,"children":3509},{"style":1222},[3510],{"type":349,"value":3511},", MetricUnit.Count, ",{"type":339,"tag":830,"props":3513,"children":3514},{"style":1231},[3515],{"type":349,"value":368},{"type":339,"tag":830,"props":3517,"children":3518},{"style":1222},[3519],{"type":349,"value":3520},");\n",{"type":339,"tag":830,"props":3522,"children":3523},{"class":832,"line":184},[3524,3529,3533,3537,3542],{"type":339,"tag":830,"props":3525,"children":3526},{"style":2855},[3527],{"type":349,"value":3528},"    return",{"type":339,"tag":830,"props":3530,"children":3531},{"style":2876},[3532],{"type":349,"value":3318},{"type":339,"tag":830,"props":3534,"children":3535},{"style":1222},[3536],{"type":349,"value":2884},{"type":339,"tag":830,"props":3538,"children":3539},{"style":1231},[3540],{"type":349,"value":3541},"201",{"type":339,"tag":830,"props":3543,"children":3544},{"style":1222},[3545],{"type":349,"value":3546},", todo);\n",{"type":339,"tag":830,"props":3548,"children":3549},{"class":832,"line":49},[3550],{"type":339,"tag":830,"props":3551,"children":3552},{"style":1222},[3553],{"type":349,"value":1326},{"type":339,"tag":830,"props":3555,"children":3556},{"class":832,"line":70},[3557],{"type":339,"tag":830,"props":3558,"children":3559},{"style":1222},[3560],{"type":349,"value":3561},"}, { logger, tracer, metrics });\n",{"type":339,"tag":345,"props":3563,"children":3564},{},[3565,3567,3573,3575,3581,3583,3589],{"type":349,"value":3566},"Gateway-level errors are rewritten to the same ",{"type":339,"tag":398,"props":3568,"children":3570},{"className":3569},[],[3571],{"type":349,"value":3572},"application/problem+json",{"type":349,"value":3574}," shape with VTL response templates, so a 401 from the authorizer looks identical to a 401 from a handler. Streaming responses are a first-class case in the contract, marked ",{"type":339,"tag":398,"props":3576,"children":3578},{"className":3577},[],[3579],{"type":349,"value":3580},"transfer: \"stream\"",{"type":349,"value":3582},", and the endpoint module switches to the streaming invoke ARN and a ",{"type":339,"tag":398,"props":3584,"children":3586},{"className":3585},[],[3587],{"type":349,"value":3588},"{proxy+}",{"type":349,"value":3590}," resource when it sees one. The retrieval endpoint uses it to stream Bedrock tokens to the browser.",{"type":339,"tag":345,"props":3592,"children":3593},{},[3594,3596,3602,3604,3610],{"type":349,"value":3595},"Adding a path is one command. ",{"type":339,"tag":398,"props":3597,"children":3599},{"className":3598},[],[3600],{"type":349,"value":3601},"pnpm api:create-path",{"type":349,"value":3603}," asks for a path and whether it buffers or streams, then writes the endpoint ",{"type":339,"tag":398,"props":3605,"children":3607},{"className":3606},[],[3608],{"type":349,"value":3609},".tf",{"type":349,"value":3611}," from a template, creates the function package, writes a starter contract, regenerates the manifest and the OpenAPI doc, and applies. You are editing business logic within a minute.",{"type":339,"tag":750,"props":3613,"children":3615},{"id":3614},"idea-4-a-sandbox-per-branch",[3616],{"type":349,"value":3617},"Idea 4: a sandbox per branch",{"type":339,"tag":345,"props":3619,"children":3620},{},[3621,3627,3628,3633,3634,3639],{"type":339,"tag":398,"props":3622,"children":3624},{"className":3623},[],[3625],{"type":349,"value":3626},"dev",{"type":349,"value":2578},{"type":339,"tag":398,"props":3629,"children":3631},{"className":3630},[],[3632],{"type":349,"value":2576},{"type":349,"value":1721},{"type":339,"tag":398,"props":3635,"children":3637},{"className":3636},[],[3638],{"type":349,"value":2584},{"type":349,"value":3640}," are fixed environments. Anything else is a sandbox, and the environment name is a short hash of the git branch:",{"type":339,"tag":820,"props":3642,"children":3644},{"className":2363,"code":3643,"language":2365,"meta":8,"style":8},"locals {\n  is_sandbox = !contains([\"dev\", \"test\", \"prod\"], var.environment)\n}\n",[3645],{"type":339,"tag":398,"props":3646,"children":3647},{"__ignoreMap":8},[3648,3656,3664],{"type":339,"tag":830,"props":3649,"children":3650},{"class":832,"line":108},[3651],{"type":339,"tag":830,"props":3652,"children":3653},{},[3654],{"type":349,"value":3655},"locals {\n",{"type":339,"tag":830,"props":3657,"children":3658},{"class":832,"line":129},[3659],{"type":339,"tag":830,"props":3660,"children":3661},{},[3662],{"type":349,"value":3663},"  is_sandbox = !contains([\"dev\", \"test\", \"prod\"], var.environment)\n",{"type":339,"tag":830,"props":3665,"children":3666},{"class":832,"line":151},[3667],{"type":339,"tag":830,"props":3668,"children":3669},{},[3670],{"type":349,"value":1168},{"type":339,"tag":345,"props":3672,"children":3673},{},[3674],{"type":339,"tag":784,"props":3675,"children":3678},{"alt":3676,"src":3677},"Environments: dev, test and prod are deployed from main through plan and apply stages, while each feature branch gets its own sandbox stack that shares dev's Bedrock knowledge base through remote state, is owned by whoever created the branch, and is nuked on Friday evening if forgotten","/articles/serverless-toolkit-sandboxes.svg",[],{"type":339,"tag":345,"props":3680,"children":3681},{},[3682,3688],{"type":339,"tag":398,"props":3683,"children":3685},{"className":3684},[],[3686],{"type":349,"value":3687},"pnpm sandbox:create",{"type":349,"value":3689}," applies a full stack for your branch. Three details make that affordable rather than reckless.",{"type":339,"tag":345,"props":3691,"children":3692},{},[3693,3695,3701,3703,3709],{"type":349,"value":3694},"Sandboxes do not get their own Bedrock knowledge base. Indexing takes a while and costs real money, so a sandbox reads dev's through ",{"type":339,"tag":398,"props":3696,"children":3698},{"className":3697},[],[3699],{"type":349,"value":3700},"terraform_remote_state",{"type":349,"value":3702},". They also skip alarms and, unless you pass ",{"type":339,"tag":398,"props":3704,"children":3706},{"className":3705},[],[3707],{"type":349,"value":3708},"--with-monitoring",{"type":349,"value":3710},", the monitoring SPA.",{"type":339,"tag":345,"props":3712,"children":3713},{},[3714],{"type":349,"value":3715},"The sandbox scripts read the owner out of Terraform state before an apply or destroy and stop if it is not you. Branch names collide more often than you would think.",{"type":339,"tag":345,"props":3717,"children":3718},{},[3719,3721,3727],{"type":349,"value":3720},"And forgotten sandboxes are nuked. A Step Functions state machine in dev runs ",{"type":339,"tag":398,"props":3722,"children":3724},{"className":3723},[],[3725],{"type":349,"value":3726},"aws-nuke",{"type":349,"value":3728}," in a Lambda on Friday evenings, looping until a pass completes clean, with a wait state for the Lambda@Edge replicas that refuse to delete for an hour after their distribution is gone. A git post-checkout hook nags you when you switch to a branch that still has a stack.",{"type":339,"tag":750,"props":3730,"children":3732},{"id":3731},"local-development",[3733],{"type":349,"value":3734},"Local development",{"type":339,"tag":345,"props":3736,"children":3737},{},[3738,3744,3746,3752,3754,3760],{"type":339,"tag":398,"props":3739,"children":3741},{"className":3740},[],[3742],{"type":349,"value":3743},"pnpm api:mock",{"type":349,"value":3745}," runs SAM's local API in Docker next to an esbuild watch on every function, repoints the generated Amplify config at localhost and restores it on exit. The catch is CORS. The deployed API answers preflight with a gateway ",{"type":339,"tag":398,"props":3747,"children":3749},{"className":3748},[],[3750],{"type":349,"value":3751},"MOCK",{"type":349,"value":3753}," integration, which SAM cannot emulate, so a ",{"type":339,"tag":398,"props":3755,"children":3757},{"className":3756},[],[3758],{"type":349,"value":3759},"local_development",{"type":349,"value":3761}," flag in Terraform swaps in a tiny Lambda that returns the same headers. It is the one place local and deployed differ, and the flag is the whole diff.",{"type":339,"tag":750,"props":3763,"children":3765},{"id":3764},"ci",[3766],{"type":349,"value":3767},"CI",{"type":339,"tag":345,"props":3769,"children":3770},{},[3771,3773,3779],{"type":349,"value":3772},"Deploys follow the same rule as the ",{"type":339,"tag":762,"props":3774,"children":3776},{"href":3775},"/articles/terraform-when-the-agent-does-the-typing",[3777],{"type":349,"value":3778},"Terraform template",{"type":349,"value":3780},": plan under a read-only role, upload the plan, apply the saved artifact behind a GitHub environment gate, never re-plan at apply time. The frontend deploy then downloads the outputs artifact from that run, regenerates the config from it without touching state, builds the SPA, syncs to S3 and invalidates CloudFront. Terraform runs once per deploy, in one job.",{"type":339,"tag":750,"props":3782,"children":3784},{"id":3783},"rough-edges",[3785],{"type":349,"value":3786},"Rough edges",{"type":339,"tag":345,"props":3788,"children":3789},{},[3790],{"type":349,"value":3791},"The community Terraform modules are pinned to commit SHAs and bumped by hand. The Lambda layer ARNs are too, with a comment admitting there is no way to automate it. There is a complete Aurora module in the repo that nothing references, kept for the projects that need a relational store. Non-prod Cognito seeds three test users with a password that is in the repo, which is fine for a template and the first thing to change in a fork. And the release tagging in the deploy workflow increments a patch version with a retry loop to survive two deploys racing, which works and which I would still rather not have written.",{"type":339,"tag":750,"props":3793,"children":3795},{"id":3794},"steal-these",[3796],{"type":349,"value":3797},"Steal these",{"type":339,"tag":345,"props":3799,"children":3800},{},[3801],{"type":349,"value":3802},"Name your Terraform outputs so a script can nest them, and let the frontend read the result. Make each function a workspace package and hash the shared code into its deployment trigger. Write the API once as a contract and derive the gateway, the client and the validator from it. Give every branch a stack, share the expensive parts with dev, and delete the rest on a schedule. None of it needs a framework. It needs about four hundred lines of Python and a naming convention.",{"type":339,"tag":2186,"props":3804,"children":3805},{},[3806],{"type":349,"value":2190},{"title":8,"searchDepth":129,"depth":129,"links":3808},[3809,3810,3811,3812,3813,3814,3815,3816,3817],{"id":2223,"depth":129,"text":2226},{"id":2331,"depth":129,"text":2334},{"id":2602,"depth":129,"text":2605},{"id":2824,"depth":129,"text":2827},{"id":3614,"depth":129,"text":3617},{"id":3731,"depth":129,"text":3734},{"id":3764,"depth":129,"text":3767},{"id":3783,"depth":129,"text":3786},{"id":3794,"depth":129,"text":3797},"content:articles:serverless-on-aws-without-a-serverless-framework.md","articles/serverless-on-aws-without-a-serverless-framework.md","articles/serverless-on-aws-without-a-serverless-framework",{"_path":3822,"_dir":724,"_draft":7,"_partial":7,"_locale":8,"title":3823,"description":3824,"published":3825,"slug":3826,"body":3827,"_type":440,"_id":4766,"_source":19,"_file":4767,"_stem":4768,"_extension":444},"/articles/the-whole-engagement-in-one-folder","The whole engagement in one folder","A consulting team's context lives in Teams threads and three people's heads. Here is how we moved it into a SharePoint library, synced it to every laptop with OneDrive, and let Claude Code keep it as an LLM wiki.","2026/6/16","the-whole-engagement-in-one-folder",{"type":336,"children":3828,"toc":4757},[3829,3834,3847,3855,3861,3866,3879,3885,3890,3898,3910,3916,3936,3957,3969,4141,4153,4161,4167,4177,4396,4401,4407,4412,4429,4455,4465,4483,4489,4494,4664,4705,4710,4716,4721,4734,4753],{"type":339,"tag":345,"props":3830,"children":3831},{},[3832],{"type":349,"value":3833},"Every consulting engagement I have been on has the same shape after week six. There is a statement of work nobody has reread since it was signed. There are forty decks, twelve of which are the current one. There is a Teams channel with the real decisions buried in it, and there are two or three people who actually know why things are the way they are. When someone new joins, one of those people loses a day.",{"type":339,"tag":345,"props":3835,"children":3836},{},[3837,3839,3845],{"type":349,"value":3838},"This post is about the setup that fixed it for us, and it is deliberately boring. No platform, no vector database, no procurement. A folder in the client's SharePoint, synced to everyone's laptop with the OneDrive client that IT already installed, and a Claude Code session opened inside it. The folder is an ",{"type":339,"tag":762,"props":3840,"children":3842},{"href":764,"rel":3841},[766],[3843],{"type":349,"value":3844},"LLM wiki",{"type":349,"value":3846}," in Andrej Karpathy's sense. The sync is free. The permissions are the client's own.",{"type":339,"tag":345,"props":3848,"children":3849},{},[3850],{"type":339,"tag":784,"props":3851,"children":3854},{"alt":3852,"src":3853},"A SharePoint document library in the client tenant, synced through OneDrive to three consultant laptops, each running a Claude Code session inside the synced folder. Teams transcripts flow in on the left","/articles/engagement-wiki-architecture.svg",[],{"type":339,"tag":750,"props":3856,"children":3858},{"id":3857},"why-sharepoint-of-all-things",[3859],{"type":349,"value":3860},"Why SharePoint, of all things",{"type":339,"tag":345,"props":3862,"children":3863},{},[3864],{"type":349,"value":3865},"I tried Notion first, then a git repo. Both failed on the same point. A consulting team on a client site does not get to choose its tools. The client tenant decides where documents may live, which SaaS is approved, and who can see what. SharePoint is already inside that boundary. Every engagement has a document library on day one, the client's information governance already applies to it, and every laptop in the team already has OneDrive syncing it. Nobody has to install anything or ask anyone.",{"type":339,"tag":345,"props":3867,"children":3868},{},[3869,3871,3877],{"type":349,"value":3870},"The second reason is that a document library is a folder. On a Mac the synced copy lives under ",{"type":339,"tag":398,"props":3872,"children":3874},{"className":3873},[],[3875],{"type":349,"value":3876},"~/Library/CloudStorage/OneDrive-\u003CTenantName>/",{"type":349,"value":3878},", and a folder is the one thing an agent is genuinely good at working with. Claude Code does not know or care that the files are being mirrored to the cloud. It reads markdown, it edits markdown, and OneDrive does the rest.",{"type":339,"tag":750,"props":3880,"children":3882},{"id":3881},"the-layout",[3883],{"type":349,"value":3884},"The layout",{"type":339,"tag":345,"props":3886,"children":3887},{},[3888],{"type":349,"value":3889},"The library is organised the way Karpathy describes, with one change for teams, which is that the log is per person.",{"type":339,"tag":820,"props":3891,"children":3893},{"code":3892},"Engagement - Acme Data Platform/\n├── CLAUDE.md               the schema: what lives where, what may not change\n├── sources/                immutable. nothing here is ever edited\n│   ├── contract/           SoW, change requests, RAID as exported\n│   ├── decks/              every deck, dated, as PDF\n│   ├── meetings/           one folder per meeting, transcript + notes\n│   └── client-docs/        architecture docs, policies, runbooks the client gave us\n├── wiki/\n│   ├── index.md            every page, one line each\n│   ├── decisions/          one page per decision, with the meeting it came from\n│   ├── people/             one page per stakeholder, role, what they care about\n│   ├── systems/            one page per client system we touch\n│   ├── workstreams/        current state of each workstream\n│   └── open-questions.md\n├── log/\n│   ├── marco.md            append-only, one line per change I made\n│   └── priya.md\n└── inbox/                  things the wiki proposed and a human has not confirmed\n",[3894],{"type":339,"tag":398,"props":3895,"children":3896},{"__ignoreMap":8},[3897],{"type":349,"value":3892},{"type":339,"tag":345,"props":3899,"children":3900},{},[3901,3902,3908],{"type":349,"value":1668},{"type":339,"tag":398,"props":3903,"children":3905},{"className":3904},[],[3906],{"type":349,"value":3907},"sources/",{"type":349,"value":3909}," rule is the one that matters. A transcript is never edited, a deck is never replaced in place. If the wiki says something wrong, the fix is to re-ingest the source or to add a newer one. That means any page can be rebuilt from scratch, and it means when two people disagree about what was said, the answer is a file with a date on it.",{"type":339,"tag":750,"props":3911,"children":3913},{"id":3912},"getting-the-meetings-in",[3914],{"type":349,"value":3915},"Getting the meetings in",{"type":339,"tag":345,"props":3917,"children":3918},{},[3919,3921,3926,3928,3934],{"type":349,"value":3920},"The wiki is only as good as what goes into ",{"type":339,"tag":398,"props":3922,"children":3924},{"className":3923},[],[3925],{"type":349,"value":3907},{"type":349,"value":3927},", and the richest source on any engagement is the meetings. Teams already transcribes them. The problem is that the transcript lands in the organiser's personal OneDrive under ",{"type":339,"tag":398,"props":3929,"children":3931},{"className":3930},[],[3932],{"type":349,"value":3933},"Recordings/",{"type":349,"value":3935},", which is exactly where nobody will find it in three weeks.",{"type":339,"tag":345,"props":3937,"children":3938},{},[3939,3941,3947,3949,3955],{"type":349,"value":3940},"A Power Automate flow moves it. The trigger is \"When a file is created\" on the organiser's ",{"type":339,"tag":398,"props":3942,"children":3944},{"className":3943},[],[3945],{"type":349,"value":3946},"Recordings",{"type":349,"value":3948}," folder, the action is \"Copy file\" into ",{"type":339,"tag":398,"props":3950,"children":3952},{"className":3951},[],[3953],{"type":349,"value":3954},"sources/meetings/",{"type":349,"value":3956}," on the library, and a naming step prefixes the date. The whole flow is four boxes and took twenty minutes, most of which was finding the folder picker.",{"type":339,"tag":345,"props":3958,"children":3959},{},[3960,3961,3967],{"type":349,"value":1668},{"type":339,"tag":398,"props":3962,"children":3964},{"className":3963},[],[3965],{"type":349,"value":3966},".vtt",{"type":349,"value":3968}," that Teams produces is not pleasant for a model to read. Timestamps every few seconds, speakers repeated on every cue, and a lot of \"yeah, yeah, no, exactly\". A small script runs on ingest and collapses it into turns:",{"type":339,"tag":820,"props":3970,"children":3972},{"code":3971,"language":1068,"meta":8,"className":1066,"style":8},"import re, sys\nfrom pathlib import Path\n\nCUE = re.compile(r\"\u003Cv ([^>]+)>(.*?)\u003C/v>\", re.S)\n\ndef turns(vtt: str):\n    speaker, buf = None, []\n    for who, text in CUE.findall(vtt):\n        text = \" \".join(text.split())\n        if who != speaker and buf:\n            yield speaker, \" \".join(buf)\n            buf = []\n        speaker, buf = who, buf + [text]\n    if buf:\n        yield speaker, \" \".join(buf)\n\nsrc = Path(sys.argv[1])\nout = src.with_suffix(\".md\")\nlines = [f\"# {src.stem}\\n\"]\nlines += [f\"**{who}:** {text}\\n\" for who, text in turns(src.read_text())]\nout.write_text(\"\\n\".join(lines))\n",[3973],{"type":339,"tag":398,"props":3974,"children":3975},{"__ignoreMap":8},[3976,3984,3992,3999,4007,4014,4022,4030,4038,4046,4054,4062,4070,4078,4086,4094,4101,4109,4117,4125,4133],{"type":339,"tag":830,"props":3977,"children":3978},{"class":832,"line":108},[3979],{"type":339,"tag":830,"props":3980,"children":3981},{},[3982],{"type":349,"value":3983},"import re, sys\n",{"type":339,"tag":830,"props":3985,"children":3986},{"class":832,"line":129},[3987],{"type":339,"tag":830,"props":3988,"children":3989},{},[3990],{"type":349,"value":3991},"from pathlib import Path\n",{"type":339,"tag":830,"props":3993,"children":3994},{"class":832,"line":151},[3995],{"type":339,"tag":830,"props":3996,"children":3997},{"emptyLinePlaceholder":641},[3998],{"type":349,"value":1794},{"type":339,"tag":830,"props":4000,"children":4001},{"class":832,"line":173},[4002],{"type":339,"tag":830,"props":4003,"children":4004},{},[4005],{"type":349,"value":4006},"CUE = re.compile(r\"\u003Cv ([^>]+)>(.*?)\u003C/v>\", re.S)\n",{"type":339,"tag":830,"props":4008,"children":4009},{"class":832,"line":140},[4010],{"type":339,"tag":830,"props":4011,"children":4012},{"emptyLinePlaceholder":641},[4013],{"type":349,"value":1794},{"type":339,"tag":830,"props":4015,"children":4016},{"class":832,"line":162},[4017],{"type":339,"tag":830,"props":4018,"children":4019},{},[4020],{"type":349,"value":4021},"def turns(vtt: str):\n",{"type":339,"tag":830,"props":4023,"children":4024},{"class":832,"line":184},[4025],{"type":339,"tag":830,"props":4026,"children":4027},{},[4028],{"type":349,"value":4029},"    speaker, buf = None, []\n",{"type":339,"tag":830,"props":4031,"children":4032},{"class":832,"line":49},[4033],{"type":339,"tag":830,"props":4034,"children":4035},{},[4036],{"type":349,"value":4037},"    for who, text in CUE.findall(vtt):\n",{"type":339,"tag":830,"props":4039,"children":4040},{"class":832,"line":70},[4041],{"type":339,"tag":830,"props":4042,"children":4043},{},[4044],{"type":349,"value":4045},"        text = \" \".join(text.split())\n",{"type":339,"tag":830,"props":4047,"children":4048},{"class":832,"line":60},[4049],{"type":339,"tag":830,"props":4050,"children":4051},{},[4052],{"type":349,"value":4053},"        if who != speaker and buf:\n",{"type":339,"tag":830,"props":4055,"children":4056},{"class":832,"line":81},[4057],{"type":339,"tag":830,"props":4058,"children":4059},{},[4060],{"type":349,"value":4061},"            yield speaker, \" \".join(buf)\n",{"type":339,"tag":830,"props":4063,"children":4064},{"class":832,"line":90},[4065],{"type":339,"tag":830,"props":4066,"children":4067},{},[4068],{"type":349,"value":4069},"            buf = []\n",{"type":339,"tag":830,"props":4071,"children":4072},{"class":832,"line":15},[4073],{"type":339,"tag":830,"props":4074,"children":4075},{},[4076],{"type":349,"value":4077},"        speaker, buf = who, buf + [text]\n",{"type":339,"tag":830,"props":4079,"children":4080},{"class":832,"line":28},[4081],{"type":339,"tag":830,"props":4082,"children":4083},{},[4084],{"type":349,"value":4085},"    if buf:\n",{"type":339,"tag":830,"props":4087,"children":4088},{"class":832,"line":221},[4089],{"type":339,"tag":830,"props":4090,"children":4091},{},[4092],{"type":349,"value":4093},"        yield speaker, \" \".join(buf)\n",{"type":339,"tag":830,"props":4095,"children":4096},{"class":832,"line":232},[4097],{"type":339,"tag":830,"props":4098,"children":4099},{"emptyLinePlaceholder":641},[4100],{"type":349,"value":1794},{"type":339,"tag":830,"props":4102,"children":4103},{"class":832,"line":196},[4104],{"type":339,"tag":830,"props":4105,"children":4106},{},[4107],{"type":349,"value":4108},"src = Path(sys.argv[1])\n",{"type":339,"tag":830,"props":4110,"children":4111},{"class":832,"line":209},[4112],{"type":339,"tag":830,"props":4113,"children":4114},{},[4115],{"type":349,"value":4116},"out = src.with_suffix(\".md\")\n",{"type":339,"tag":830,"props":4118,"children":4119},{"class":832,"line":243},[4120],{"type":339,"tag":830,"props":4121,"children":4122},{},[4123],{"type":349,"value":4124},"lines = [f\"# {src.stem}\\n\"]\n",{"type":339,"tag":830,"props":4126,"children":4127},{"class":832,"line":1449},[4128],{"type":339,"tag":830,"props":4129,"children":4130},{},[4131],{"type":349,"value":4132},"lines += [f\"**{who}:** {text}\\n\" for who, text in turns(src.read_text())]\n",{"type":339,"tag":830,"props":4134,"children":4135},{"class":832,"line":1458},[4136],{"type":339,"tag":830,"props":4137,"children":4138},{},[4139],{"type":349,"value":4140},"out.write_text(\"\\n\".join(lines))\n",{"type":339,"tag":345,"props":4142,"children":4143},{},[4144,4146,4151],{"type":349,"value":4145},"An hour-long meeting goes from about nine thousand lines of VTT to roughly two hundred lines of markdown, with the same words. The ",{"type":339,"tag":398,"props":4147,"children":4149},{"className":4148},[],[4150],{"type":349,"value":3966},{"type":349,"value":4152}," stays next to it, unedited, because that is the source.",{"type":339,"tag":345,"props":4154,"children":4155},{},[4156],{"type":339,"tag":784,"props":4157,"children":4160},{"alt":4158,"src":4159},"Lifecycle of one meeting: Teams records it, Power Automate copies the transcript into sources, OneDrive syncs it down, whoever added it runs an ingest, the wiki and log update, OneDrive syncs it back up, and a colleague queries it the next morning","/articles/engagement-wiki-lifecycle.svg",[],{"type":339,"tag":750,"props":4162,"children":4164},{"id":4163},"the-schema",[4165],{"type":349,"value":4166},"The schema",{"type":339,"tag":345,"props":4168,"children":4169},{},[4170,4175],{"type":339,"tag":398,"props":4171,"children":4173},{"className":4172},[],[4174],{"type":349,"value":777},{"type":349,"value":4176}," at the root is what turns a folder of files into a wiki. Claude Code reads it at the start of every session opened inside the folder, so every team member gets the same rules without being told them. Ours is about a hundred lines. The parts that do the most work are these:",{"type":339,"tag":820,"props":4178,"children":4181},{"code":4179,"language":440,"meta":8,"className":4180,"style":8},"## Operations\n\n- **ingest**: for each file in sources/ not yet listed in log/*.md, read it,\n  update or create the wiki pages it affects, add a line to index.md if a page\n  is new, and append one line per touched page to log/\u003Cyour-name>.md.\n  Never summarise a source into a single page. Spread it across the pages it\n  belongs to (a meeting usually touches decisions/, people/ and a workstream).\n- **query**: answer from wiki/ first. Every claim gets a citation in the form\n  (sources/meetings/2026-05-12-design-review/transcript.md). If the wiki does\n  not know, say so and name the source that would.\n- **lint**: find pages that contradict each other or a newer source, decisions\n  with no source, people pages not touched in 30 days, and open questions that\n  a later meeting answered. Write the findings to inbox/lint.md as checkboxes.\n\n## Rules\n\n- sources/ is read-only. Do not edit, rename or move anything in it.\n- A decision page must name the meeting it came from and who made the call.\n- Do not create a page for a person from a single mention. Two sources or a\n  human confirmation.\n- Anything you are not sure belongs in the wiki goes in inbox/, not in wiki/.\n","language-markdown shiki shiki-themes github-dark",[4182],{"type":339,"tag":398,"props":4183,"children":4184},{"__ignoreMap":8},[4185,4194,4201,4220,4228,4236,4244,4252,4269,4277,4285,4302,4310,4318,4325,4333,4340,4352,4364,4376,4384],{"type":339,"tag":830,"props":4186,"children":4187},{"class":832,"line":108},[4188],{"type":339,"tag":830,"props":4189,"children":4191},{"style":4190},"--shiki-default:#79B8FF;--shiki-default-font-weight:bold",[4192],{"type":349,"value":4193},"## Operations\n",{"type":339,"tag":830,"props":4195,"children":4196},{"class":832,"line":129},[4197],{"type":339,"tag":830,"props":4198,"children":4199},{"emptyLinePlaceholder":641},[4200],{"type":349,"value":1794},{"type":339,"tag":830,"props":4202,"children":4203},{"class":832,"line":151},[4204,4209,4215],{"type":339,"tag":830,"props":4205,"children":4206},{"style":3370},[4207],{"type":349,"value":4208},"-",{"type":339,"tag":830,"props":4210,"children":4212},{"style":4211},"--shiki-default:#E1E4E8;--shiki-default-font-weight:bold",[4213],{"type":349,"value":4214}," **ingest**",{"type":339,"tag":830,"props":4216,"children":4217},{"style":1222},[4218],{"type":349,"value":4219},": for each file in sources/ not yet listed in log/*.md, read it,\n",{"type":339,"tag":830,"props":4221,"children":4222},{"class":832,"line":173},[4223],{"type":339,"tag":830,"props":4224,"children":4225},{"style":1222},[4226],{"type":349,"value":4227},"  update or create the wiki pages it affects, add a line to index.md if a page\n",{"type":339,"tag":830,"props":4229,"children":4230},{"class":832,"line":140},[4231],{"type":339,"tag":830,"props":4232,"children":4233},{"style":1222},[4234],{"type":349,"value":4235},"  is new, and append one line per touched page to log/\u003Cyour-name>.md.\n",{"type":339,"tag":830,"props":4237,"children":4238},{"class":832,"line":162},[4239],{"type":339,"tag":830,"props":4240,"children":4241},{"style":1222},[4242],{"type":349,"value":4243},"  Never summarise a source into a single page. Spread it across the pages it\n",{"type":339,"tag":830,"props":4245,"children":4246},{"class":832,"line":184},[4247],{"type":339,"tag":830,"props":4248,"children":4249},{"style":1222},[4250],{"type":349,"value":4251},"  belongs to (a meeting usually touches decisions/, people/ and a workstream).\n",{"type":339,"tag":830,"props":4253,"children":4254},{"class":832,"line":49},[4255,4259,4264],{"type":339,"tag":830,"props":4256,"children":4257},{"style":3370},[4258],{"type":349,"value":4208},{"type":339,"tag":830,"props":4260,"children":4261},{"style":4211},[4262],{"type":349,"value":4263}," **query**",{"type":339,"tag":830,"props":4265,"children":4266},{"style":1222},[4267],{"type":349,"value":4268},": answer from wiki/ first. Every claim gets a citation in the form\n",{"type":339,"tag":830,"props":4270,"children":4271},{"class":832,"line":70},[4272],{"type":339,"tag":830,"props":4273,"children":4274},{"style":1222},[4275],{"type":349,"value":4276},"  (sources/meetings/2026-05-12-design-review/transcript.md). If the wiki does\n",{"type":339,"tag":830,"props":4278,"children":4279},{"class":832,"line":60},[4280],{"type":339,"tag":830,"props":4281,"children":4282},{"style":1222},[4283],{"type":349,"value":4284},"  not know, say so and name the source that would.\n",{"type":339,"tag":830,"props":4286,"children":4287},{"class":832,"line":81},[4288,4292,4297],{"type":339,"tag":830,"props":4289,"children":4290},{"style":3370},[4291],{"type":349,"value":4208},{"type":339,"tag":830,"props":4293,"children":4294},{"style":4211},[4295],{"type":349,"value":4296}," **lint**",{"type":339,"tag":830,"props":4298,"children":4299},{"style":1222},[4300],{"type":349,"value":4301},": find pages that contradict each other or a newer source, decisions\n",{"type":339,"tag":830,"props":4303,"children":4304},{"class":832,"line":90},[4305],{"type":339,"tag":830,"props":4306,"children":4307},{"style":1222},[4308],{"type":349,"value":4309},"  with no source, people pages not touched in 30 days, and open questions that\n",{"type":339,"tag":830,"props":4311,"children":4312},{"class":832,"line":15},[4313],{"type":339,"tag":830,"props":4314,"children":4315},{"style":1222},[4316],{"type":349,"value":4317},"  a later meeting answered. Write the findings to inbox/lint.md as checkboxes.\n",{"type":339,"tag":830,"props":4319,"children":4320},{"class":832,"line":28},[4321],{"type":339,"tag":830,"props":4322,"children":4323},{"emptyLinePlaceholder":641},[4324],{"type":349,"value":1794},{"type":339,"tag":830,"props":4326,"children":4327},{"class":832,"line":221},[4328],{"type":339,"tag":830,"props":4329,"children":4330},{"style":4190},[4331],{"type":349,"value":4332},"## Rules\n",{"type":339,"tag":830,"props":4334,"children":4335},{"class":832,"line":232},[4336],{"type":339,"tag":830,"props":4337,"children":4338},{"emptyLinePlaceholder":641},[4339],{"type":349,"value":1794},{"type":339,"tag":830,"props":4341,"children":4342},{"class":832,"line":196},[4343,4347],{"type":339,"tag":830,"props":4344,"children":4345},{"style":3370},[4346],{"type":349,"value":4208},{"type":339,"tag":830,"props":4348,"children":4349},{"style":1222},[4350],{"type":349,"value":4351}," sources/ is read-only. Do not edit, rename or move anything in it.\n",{"type":339,"tag":830,"props":4353,"children":4354},{"class":832,"line":209},[4355,4359],{"type":339,"tag":830,"props":4356,"children":4357},{"style":3370},[4358],{"type":349,"value":4208},{"type":339,"tag":830,"props":4360,"children":4361},{"style":1222},[4362],{"type":349,"value":4363}," A decision page must name the meeting it came from and who made the call.\n",{"type":339,"tag":830,"props":4365,"children":4366},{"class":832,"line":243},[4367,4371],{"type":339,"tag":830,"props":4368,"children":4369},{"style":3370},[4370],{"type":349,"value":4208},{"type":339,"tag":830,"props":4372,"children":4373},{"style":1222},[4374],{"type":349,"value":4375}," Do not create a page for a person from a single mention. Two sources or a\n",{"type":339,"tag":830,"props":4377,"children":4378},{"class":832,"line":1449},[4379],{"type":339,"tag":830,"props":4380,"children":4381},{"style":1222},[4382],{"type":349,"value":4383},"  human confirmation.\n",{"type":339,"tag":830,"props":4385,"children":4386},{"class":832,"line":1458},[4387,4391],{"type":339,"tag":830,"props":4388,"children":4389},{"style":3370},[4390],{"type":349,"value":4208},{"type":339,"tag":830,"props":4392,"children":4393},{"style":1222},[4394],{"type":349,"value":4395}," Anything you are not sure belongs in the wiki goes in inbox/, not in wiki/.\n",{"type":339,"tag":345,"props":4397,"children":4398},{},[4399],{"type":349,"value":4400},"The two operations we use most are ingest and query. Whoever adds a source runs the ingest, in their own session, the same day. Everyone runs queries. A new joiner's first afternoon is now \"open a session in the folder and ask it why we chose event sourcing for the orders domain\", and the answer comes back with the date of the meeting and the name of the architect who pushed for it.",{"type":339,"tag":750,"props":4402,"children":4404},{"id":4403},"what-onedrive-does-to-you",[4405],{"type":349,"value":4406},"What OneDrive does to you",{"type":339,"tag":345,"props":4408,"children":4409},{},[4410],{"type":349,"value":4411},"This is the part I wish someone had written down.",{"type":339,"tag":345,"props":4413,"children":4414},{},[4415,4420,4422,4427],{"type":339,"tag":2072,"props":4416,"children":4417},{},[4418],{"type":349,"value":4419},"Files On-Demand.",{"type":349,"value":4421}," By default OneDrive on macOS keeps cloud-only placeholders and downloads a file when something opens it. Claude Code's Read tool triggers a download, but a Grep across ",{"type":339,"tag":398,"props":4423,"children":4425},{"className":4424},[],[4426],{"type":349,"value":3907},{"type":349,"value":4428}," will silently search only what has been hydrated. Right-click the engagement folder, choose \"Always Keep on This Device\", and check Finder shows the solid tick on every file before you trust a search. We lost half a day to a transcript that \"did not exist\" because it had never been downloaded.",{"type":339,"tag":345,"props":4430,"children":4431},{},[4432,4437,4439,4445,4447,4453],{"type":339,"tag":2072,"props":4433,"children":4434},{},[4435],{"type":349,"value":4436},"Conflicts.",{"type":349,"value":4438}," OneDrive does not merge. If two people edit ",{"type":339,"tag":398,"props":4440,"children":4442},{"className":4441},[],[4443],{"type":349,"value":4444},"index.md",{"type":349,"value":4446}," within the same sync window, the second one gets a copy named ",{"type":339,"tag":398,"props":4448,"children":4450},{"className":4449},[],[4451],{"type":349,"value":4452},"index-Marco's MacBook Pro.md",{"type":349,"value":4454}," and no warning worth the name. Three rules made this a non-problem. Many small pages rather than a few big ones, so two people rarely touch the same file. One log file per person, never a shared one. And ingest is run by the person who added the source, so two people are never ingesting the same meeting at the same time.",{"type":339,"tag":345,"props":4456,"children":4457},{},[4458,4463],{"type":339,"tag":2072,"props":4459,"children":4460},{},[4461],{"type":349,"value":4462},"Latency.",{"type":349,"value":4464}," A change takes anywhere from five seconds to a couple of minutes to appear on a colleague's machine, depending on what the client's network is doing. The lint operation catches the case where someone queried a page that was about to change. It is not real-time and it does not need to be.",{"type":339,"tag":345,"props":4466,"children":4467},{},[4468,4473,4475,4481],{"type":339,"tag":2072,"props":4469,"children":4470},{},[4471],{"type":349,"value":4472},"Paths with spaces.",{"type":349,"value":4474}," The synced root will be something like ",{"type":339,"tag":398,"props":4476,"children":4478},{"className":4477},[],[4479],{"type":349,"value":4480},"OneDrive-AcmeCorp/Engagement - Acme Data Platform/",{"type":349,"value":4482},". Every script quotes its paths. Every script.",{"type":339,"tag":750,"props":4484,"children":4486},{"id":4485},"what-the-model-is-not-allowed-to-see",[4487],{"type":349,"value":4488},"What the model is not allowed to see",{"type":339,"tag":345,"props":4490,"children":4491},{},[4492],{"type":349,"value":4493},"The client's data stays in the client's tenant, which is the whole point of using their SharePoint. But the folder is on your laptop, and the agent reads what is on your laptop, so the deny list still matters. Ours keeps commercial documents out of the model entirely:",{"type":339,"tag":820,"props":4495,"children":4497},{"code":4496,"language":18,"meta":8,"className":1211,"style":8},"{\n  \"permissions\": {\n    \"deny\": [\n      \"Read(./sources/contract/rates/**)\",\n      \"Read(./sources/client-docs/hr/**)\"\n    ]\n  },\n  \"sandbox\": {\n    \"enabled\": true,\n    \"filesystem\": {\n      \"denyRead\": [\n        \"/Users/marco/Library/CloudStorage/OneDrive-AcmeCorp/Engagement - Acme Data Platform/sources/contract/rates/\",\n        \"/Users/marco/Library/CloudStorage/OneDrive-AcmeCorp/Engagement - Acme Data Platform/sources/client-docs/hr/\"\n      ]\n    }\n  }\n}\n",[4498],{"type":339,"tag":398,"props":4499,"children":4500},{"__ignoreMap":8},[4501,4508,4519,4530,4542,4550,4557,4564,4575,4594,4605,4616,4628,4636,4643,4650,4657],{"type":339,"tag":830,"props":4502,"children":4503},{"class":832,"line":108},[4504],{"type":339,"tag":830,"props":4505,"children":4506},{"style":1222},[4507],{"type":349,"value":1225},{"type":339,"tag":830,"props":4509,"children":4510},{"class":832,"line":129},[4511,4515],{"type":339,"tag":830,"props":4512,"children":4513},{"style":1231},[4514],{"type":349,"value":1234},{"type":339,"tag":830,"props":4516,"children":4517},{"style":1222},[4518],{"type":349,"value":1239},{"type":339,"tag":830,"props":4520,"children":4521},{"class":832,"line":151},[4522,4526],{"type":339,"tag":830,"props":4523,"children":4524},{"style":1231},[4525],{"type":349,"value":1247},{"type":339,"tag":830,"props":4527,"children":4528},{"style":1222},[4529],{"type":349,"value":1252},{"type":339,"tag":830,"props":4531,"children":4532},{"class":832,"line":173},[4533,4538],{"type":339,"tag":830,"props":4534,"children":4535},{"style":1258},[4536],{"type":349,"value":4537},"      \"Read(./sources/contract/rates/**)\"",{"type":339,"tag":830,"props":4539,"children":4540},{"style":1222},[4541],{"type":349,"value":1266},{"type":339,"tag":830,"props":4543,"children":4544},{"class":832,"line":140},[4545],{"type":339,"tag":830,"props":4546,"children":4547},{"style":1258},[4548],{"type":349,"value":4549},"      \"Read(./sources/client-docs/hr/**)\"\n",{"type":339,"tag":830,"props":4551,"children":4552},{"class":832,"line":162},[4553],{"type":339,"tag":830,"props":4554,"children":4555},{"style":1222},[4556],{"type":349,"value":1318},{"type":339,"tag":830,"props":4558,"children":4559},{"class":832,"line":184},[4560],{"type":339,"tag":830,"props":4561,"children":4562},{"style":1222},[4563],{"type":349,"value":1326},{"type":339,"tag":830,"props":4565,"children":4566},{"class":832,"line":49},[4567,4571],{"type":339,"tag":830,"props":4568,"children":4569},{"style":1231},[4570],{"type":349,"value":1489},{"type":339,"tag":830,"props":4572,"children":4573},{"style":1222},[4574],{"type":349,"value":1239},{"type":339,"tag":830,"props":4576,"children":4577},{"class":832,"line":70},[4578,4582,4586,4590],{"type":339,"tag":830,"props":4579,"children":4580},{"style":1231},[4581],{"type":349,"value":1502},{"type":339,"tag":830,"props":4583,"children":4584},{"style":1222},[4585],{"type":349,"value":1371},{"type":339,"tag":830,"props":4587,"children":4588},{"style":1231},[4589],{"type":349,"value":390},{"type":339,"tag":830,"props":4591,"children":4592},{"style":1222},[4593],{"type":349,"value":1266},{"type":339,"tag":830,"props":4595,"children":4596},{"class":832,"line":60},[4597,4601],{"type":339,"tag":830,"props":4598,"children":4599},{"style":1231},[4600],{"type":349,"value":1545},{"type":339,"tag":830,"props":4602,"children":4603},{"style":1222},[4604],{"type":349,"value":1239},{"type":339,"tag":830,"props":4606,"children":4607},{"class":832,"line":81},[4608,4612],{"type":339,"tag":830,"props":4609,"children":4610},{"style":1231},[4611],{"type":349,"value":1558},{"type":339,"tag":830,"props":4613,"children":4614},{"style":1222},[4615],{"type":349,"value":1252},{"type":339,"tag":830,"props":4617,"children":4618},{"class":832,"line":90},[4619,4624],{"type":339,"tag":830,"props":4620,"children":4621},{"style":1258},[4622],{"type":349,"value":4623},"        \"/Users/marco/Library/CloudStorage/OneDrive-AcmeCorp/Engagement - Acme Data Platform/sources/contract/rates/\"",{"type":339,"tag":830,"props":4625,"children":4626},{"style":1222},[4627],{"type":349,"value":1266},{"type":339,"tag":830,"props":4629,"children":4630},{"class":832,"line":15},[4631],{"type":339,"tag":830,"props":4632,"children":4633},{"style":1258},[4634],{"type":349,"value":4635},"        \"/Users/marco/Library/CloudStorage/OneDrive-AcmeCorp/Engagement - Acme Data Platform/sources/client-docs/hr/\"\n",{"type":339,"tag":830,"props":4637,"children":4638},{"class":832,"line":28},[4639],{"type":339,"tag":830,"props":4640,"children":4641},{"style":1222},[4642],{"type":349,"value":1632},{"type":339,"tag":830,"props":4644,"children":4645},{"class":832,"line":221},[4646],{"type":339,"tag":830,"props":4647,"children":4648},{"style":1222},[4649],{"type":349,"value":1641},{"type":339,"tag":830,"props":4651,"children":4652},{"class":832,"line":232},[4653],{"type":339,"tag":830,"props":4654,"children":4655},{"style":1222},[4656],{"type":349,"value":1650},{"type":339,"tag":830,"props":4658,"children":4659},{"class":832,"line":196},[4660],{"type":339,"tag":830,"props":4661,"children":4662},{"style":1222},[4663],{"type":349,"value":1168},{"type":339,"tag":345,"props":4665,"children":4666},{},[4667,4669,4675,4677,4682,4684,4689,4691,4696,4698,4703],{"type":349,"value":4668},"That ",{"type":339,"tag":398,"props":4670,"children":4672},{"className":4671},[],[4673],{"type":349,"value":4674},".claude/settings.json",{"type":349,"value":4676}," lives in the folder and syncs with it, so the deny list is the same on every laptop. The sandbox ",{"type":339,"tag":398,"props":4678,"children":4680},{"className":4679},[],[4681],{"type":349,"value":1697},{"type":349,"value":4683}," block is there because ",{"type":339,"tag":398,"props":4685,"children":4687},{"className":4686},[],[4688],{"type":349,"value":1674},{"type":349,"value":4690}," only stops the file tools, not a shell ",{"type":339,"tag":398,"props":4692,"children":4694},{"className":4693},[],[4695],{"type":349,"value":1682},{"type":349,"value":4697},". I have written about that distinction ",{"type":339,"tag":762,"props":4699,"children":4700},{"href":723},[4701],{"type":349,"value":4702},"before",{"type":349,"value":4704}," and it applies twice as hard when the folder is not yours.",{"type":339,"tag":345,"props":4706,"children":4707},{},[4708],{"type":349,"value":4709},"Two things you should also check with the client before starting. Which model provider the tenant has approved, and whether the SharePoint library carries a sensitivity label that forbids exactly this. Both are five-minute conversations if you have them in week one and month-long ones if you have them in week ten.",{"type":339,"tag":750,"props":4711,"children":4713},{"id":4712},"what-changed",[4714],{"type":349,"value":4715},"What changed",{"type":339,"tag":345,"props":4717,"children":4718},{},[4719],{"type":349,"value":4720},"The measurable thing is onboarding. The last joiner was productive on day two rather than in week two. The less measurable thing is that arguments about what was agreed now end in about a minute, because someone asks the wiki and it cites a transcript.",{"type":339,"tag":345,"props":4722,"children":4723},{},[4724,4726,4732],{"type":349,"value":4725},"The thing I did not expect is that the wiki caught the client contradicting themselves. The lint flagged that a data residency requirement stated in a March workshop was missing from a May architecture review, and it was right. That is a page in ",{"type":339,"tag":398,"props":4727,"children":4729},{"className":4728},[],[4730],{"type":349,"value":4731},"decisions/",{"type":349,"value":4733}," now, with both sources cited, and it saved a rework we would have found in testing.",{"type":339,"tag":345,"props":4735,"children":4736},{},[4737,4739,4744,4746,4751],{"type":349,"value":4738},"If you are going to build one, start with the meetings. Get the Power Automate flow running in the first week, before there is a backlog. Write ",{"type":339,"tag":398,"props":4740,"children":4742},{"className":4741},[],[4743],{"type":349,"value":777},{"type":349,"value":4745}," before the first ingest, and make the ",{"type":339,"tag":398,"props":4747,"children":4749},{"className":4748},[],[4750],{"type":349,"value":3907},{"type":349,"value":4752}," rule the first line. Then open a session in the folder and let it do the filing.",{"type":339,"tag":2186,"props":4754,"children":4755},{},[4756],{"type":349,"value":2190},{"title":8,"searchDepth":129,"depth":129,"links":4758},[4759,4760,4761,4762,4763,4764,4765],{"id":3857,"depth":129,"text":3860},{"id":3881,"depth":129,"text":3884},{"id":3912,"depth":129,"text":3915},{"id":4163,"depth":129,"text":4166},{"id":4403,"depth":129,"text":4406},{"id":4485,"depth":129,"text":4488},{"id":4712,"depth":129,"text":4715},"content:articles:the-whole-engagement-in-one-folder.md","articles/the-whole-engagement-in-one-folder.md","articles/the-whole-engagement-in-one-folder",1789320761472]