The Pro plan gives you everything in a Free plan with more usage and the full set of Claude capabilities. That includes Claude Code, Claude Cowork, Claude Design, along with projects to organize your chats and documents, access to more Claude models, and Claude for Microsoft 365. You can choose a monthly or annual subscription.
The Max plan is built for people who work with Claude throughout the day and want longer, uninterrupted sessions. You get everything in Pro, plus more usage: choose from 5x or 20x the usage of Pro per 5-hour session. Max also gives you higher output limits. Both options are billed monthly. See the cards above for current pricing.
Every plan has usage limits that reset on a rolling five-hour session window, and paid plans add weekly limits on top. Your activity across Claude on web, desktop, mobile, and Claude Code all draws from the same pool. How much you can do depends on the length and complexity of your conversations, the model you choose, and the features you use, so there's no fixed message count. Free covers everyday questions. Pro gives you at least 5x more usage per 5-hour session than Free. Max gives you 5x or 20x more usage per 5-hour session than Pro. On Team plans, Standard seats give more than Pro and Premium seats give 5x more than Standard. To manage capacity and make sure all users have fair access, we may limit your usage in other ways, such as weekly and monthly caps or model and feature usage, at our discretion. When you reach a limit, you can wait for it to reset, move to a higher plan, or, on paid plans, turn on usage credits to keep working at standard API rates. You can see where you stand anytime in Settings > Usage.
Claude Code is included in all paid plans. It shares the same usage limits as the rest of your plan, so your work in the terminal and your chats draw from one pool. For heavy coding sessions, you can also switch to pay-as-you-go API credits through a Console account.
The Claude Enterprise plan is built for large businesses operating at scale. Pricing is $20 per seat per month plus usage billed at API rates, so cost scales with the models and tasks your team runs. Enterprise is billed annually and includes everything in Team plus advanced security and admin controls, including SCIM, audit logs, custom data retention, and role-based access. You can get started self-serve or contact sales for a tailored quote.
Billing and payments
Yes, annual plans are designed to cost less than monthly subscriptions. Annual subscriptions are available for Pro and Team plans. Enterprise plans are only available as annual subscriptions.
When you choose to pay annually, you're billed once up front for the year. If you're already on a monthly plan, you can switch to annual in Settings > Billing, and any unused time is credited toward your first annual invoice. See the steps for Pro and Team.
Payments are generally non-refundable, except where our Consumer Terms of Service say otherwise or where local law requires it.
Customers outside of Europe can follow this process to request a refund:
Log in, open Get help from the menu, start a message, and choose Claude Refund Request to check your eligibility.
If you subscribed through the App Store, Apple handles the refund, so you'll request it from Apple.
If you subscribed through Google Play, contact our Support team to check eligibility for an active subscription.
If you're in the European Economic Area and the United Kingdom, and within your 14-day withdrawal period, you can request a refund directly in the app.
Managing your plan
You can change your plan any time in Settings.
For individual plans, go to Settings > Billing and choose Manage subscription.
On Team plans, an Owner or Primary Owner manages this under Organization settings > Billing. See more details here.
Upgrades take effect right away, and any unused time on your current plan is credited toward the new one, so you won't pay twice for the same days.
Moving from monthly to annual works the same way, with your remaining time credited to the first annual invoice.
In some upgrades (for example Pro Annual to Max 5x Monthly), your remaining value on your old plan is larger than the cost of one billing period of the new plan. In this case, the excess will be stored as credit, which will automatically get used for future purchases.
Downgrades take effect at the end of your current billing period, and your chats, projects, and files stay with your account whichever plan you're on.
You can cancel anytime, and your plan stays active until the end of your current billing period. To avoid the next charge, cancel at least 24 hours before your renewal date.
If you signed up on the web or the desktop app, go to Settings > Billing and click Cancel.
If you subscribed through the App Store or Google Play, cancel from that app under Billing > Manage subscription, or directly in your Apple or Google Play account.
Sales-assisted Enterprise: Reach out to your Anthropic Contact or our Sales team to discuss cancellation.
Canceling doesn't delete your data. Your chats, projects, and files stay with your account, though some features aren't available on the Free plan. For Pro and Max, your account moves to the Free plan once the period ends. For step-by-step help, see how to cancel a Pro or Max plan or cancel a Team plan.
Plans & Pricing | Claude by Anthropic
\ No newline at end of file
diff --git a/data/artifacts/87cdcea7dd7bfc4e554d3fe4e8d3377ca8a47110130537414c82cc9ac61fdd80 b/data/artifacts/87cdcea7dd7bfc4e554d3fe4e8d3377ca8a47110130537414c82cc9ac61fdd80
new file mode 100644
index 0000000..de68b16
--- /dev/null
+++ b/data/artifacts/87cdcea7dd7bfc4e554d3fe4e8d3377ca8a47110130537414c82cc9ac61fdd80
@@ -0,0 +1 @@
+Introducing Claude Sonnet 5 \ Anthropic
Claude Sonnet 5 is built to be the most agentic Sonnet model yet. It can make plans, use tools like browsers and terminals, and run autonomously at a level that, just a few months ago, required larger and more expensive models.
For many developers, the agentic AI era began with Sonnet-class models: Claude Sonnet 3.5, 3.6, and 3.7 were the first models that showed impressive skills in coding and tool use. More recently, though, the clearest gains in agentic capabilities have been in our Opus-class models.
Sonnet 5 narrows the gap: its performance is close to that of Opus 4.8, but at lower prices. It’s a substantial improvement over its predecessor, Sonnet 4.6, on important aspects of agentic performance like reasoning, tool use, coding, and knowledge work:
Scores for Sonnet 5 on a variety of evaluations compared to those of Sonnet 4.6 and Opus 4.8 (a more generally capable model, for reference). The Claude Sonnet 5 System Card reports a broader set of evaluations in detail.
Our safety assessments found that Sonnet 5 shows an overall lower rate of undesirable behaviors than Sonnet 4.6, and is generally safer to use in agentic contexts. Evaluations also show that it has a much lower ability to perform cybersecurity tasks than our current Opus models.
From today, Claude Sonnet 5 is available across all plans: it is the default model for Free and Pro plans, and is available to Max, Team, and Enterprise users. It is priced at $2 per million input tokens and $10 per million output tokens. Developers can use claude-sonnet-5 via the Claude API.
Working with Claude Sonnet 5
The charts below compare the performance of Sonnet 5 with Sonnet 4.6 and Opus 4.8 at different effort levels on the agentic search evaluation BrowseComp and the computer use evaluation OSWorld-Verified. Sonnet 5 (orange line) is a strict improvement over Sonnet 4.6 (gray line) and covers a much wider range of cost-performance options than Opus 4.8 (yellow line). It provides substantially improved cost efficiency at medium effort; its higher-effort performance can match Opus 4.8 on some tasks. Between Sonnet 5 and Opus 4.8, users can adjust the effort level to find the right balance of cost and performance.
Cost-performance curves at different effort levels. The previous best Sonnet model (Sonnet 4.6) fell well short of Opus 4.8. Sonnet 5 offers a wider range of cost-performance options than Sonnet 4.6, and in some cases matches Opus 4.8’s capability levels. The charts show Sonnet 5 priced at $3 per million input tokens and $15 per million output tokens (existing standard pricing). Sonnet 5's introductory pricing of $2/MTok input and $10/MTok output has since been made permanent, so its actual cost is lower than shown here. Opus 4.8 is priced at $5/MTok input and $25/MTok output. xhigh = extra high effort level.
Cost-performance curves at different effort levels. The previous best Sonnet model (Sonnet 4.6) fell well short of Opus 4.8. Sonnet 5 offers a wider range of cost-performance options than Sonnet 4.6, and in some cases matches Opus 4.8’s capability levels. The charts show Sonnet 5 priced at $3 per million input tokens and $15 per million output tokens (existing standard pricing). Sonnet 5's introductory pricing of $2/MTok input and $10/MTok output has since been made permanent, so its actual cost is lower than shown here. Opus 4.8 is priced at $5/MTok input and $25/MTok output. xhigh = extra high effort level.
Feedback from our early access partners has been consistent: Sonnet 5 is much more agentic than its predecessors. Testers described how it finishes complex tasks where previous Sonnet models would stop short, how it checks its own output without explicitly being asked, and how it does all this agentic work at an attractive price point:
Claude Sonnet 5 gives our agents a strong execution layer for multi-step software engineering work. It handles sustained coding, tool use, and debugging well across messy technical contexts, and has been especially useful for workflows where follow-through and technical grounding matter.
We handed Claude Sonnet 5 a two-part job—update Salesforce account tiers, send a launch announcement to enterprise contacts—and it finished end to end. That used to stall halfway. For day-to-day automation, it’s a no-brainer.
Claude Sonnet 5 gets more done with less. Same output quality, fewer steps to get there. It refuses unsafe requests cleanly and consistently, too. At Lovable, we’re putting powerful tools in the hands of millions of builders. A model that knows when to say no is just as important as one that knows how to build.
We ran Claude Sonnet 5 against dozens of our most challenging real pull requests, and it carried each one through to a tested, verified result on its own — freeing our engineers to focus on the judgment, the decision, and the final sign-off.
I asked Claude Sonnet 5 to investigate a bug. Unprompted, it wrote a reproducing test, implemented the fix, then stashed it to confirm the bug came back without the change. All in a single pass.
With Claude Sonnet 5, agents stay on plan, follow our conventions, and ship clean multi-step changes, all at an efficient cost.
Claude Sonnet 5 is at its best on brownfield code—race conditions, hidden tests, the parts nobody wants to touch. It traces a failure to its actual root cause and ships a durable fix instead of patching the symptom.
Claude Sonnet 5 sits on the Pareto frontier for Eve’s plaintiff-law tasks. We see the clearest gains in legal research and analysis, at a price-to-performance ratio that made the choice to migrate easy.
ClickHouse agents explore live data and produce insights on the fly, so time-to-insight matters when testing new models. Claude Sonnet 5 reasons in tighter steps and gets our users to answers noticeably faster. That speed is a difference our customers feel.
At Pace, our computer-use agents run insurance workflows—submission intake, FNOL, loss runs—on the systems our operations teams already use. Claude Sonnet 5 consistently takes the right action and does it quickly, which is what real insurance work demands.
For enterprise teams managing high-volume, complex workloads, Claude Sonnet 5 represents a genuine step forward — strong performance where it counts, with the speed and cost profile that makes scaling practical. On several complex tasks it exceeds the current frontier, while delivering fast responses at low costs. For enterprises running at scale, that's a real operational win.
Claude Sonnet 5 handled the full range of coding tasks we tested it on, while resolving more issues. It's a meaningful improvement to both quality and efficiency.
Claude Sonnet 5 is a strong agentic-coding model, delivering top-tier accuracy comparable to Opus-class models and a clear step-function improvement over Sonnet 4.6. It conducts thorough explorations and sustains focus noticeably longer on complex tasks.
01 / 13
Safety evaluations
Our pre-deployment safety evaluations found that Sonnet 5 was overall an improvement on Sonnet 4.6. On agentic safety, the model is better at refusing malicious requests and resisting hijack attempts in prompt injection attacks. The model shows lower rates of hallucination and sycophancy than Sonnet 4.6. On our automated behavioral audit, which tests a wide range of misaligned behaviors such as cooperation with misuse and deception, Sonnet 5 scored lower (that is, safer) overall. However, it did show somewhat higher rates of misaligned behavior on this assessment compared to the more capable Opus 4.8 and Claude Mythos Preview.
Rates of misaligned behavior on our automated behavioral audit, which tests for a very wide range of undesirable behaviors across many situations and contexts (see Section 6.4 of the Sonnet 5 System Card for a complete list and results for each specific behavior). Sonnet 5 shows an overall lower rate of misaligned behavior than Sonnet 4.6, though a higher rate than Mythos Preview and Opus 4.8.
We did not deliberately train Sonnet 5 on cybersecurity tasks. It can perform some routine, non-harmful cyber tasks, but on evaluations testing potentially dangerous cyber skills, such as developing software exploits, it shows substantially poorer performance than models such as Opus 4.8 and Mythos 5. Scores from one evaluation, which tested models’ ability to develop exploits for vulnerabilities in the Firefox browser, are shown in the chart below. Sonnet 5 was never able to develop a full working exploit, but it does show a slightly higher rate of partial success than Sonnet 4.6. This latter change is likely due to improvements in general intelligence rather than specific training.
Scores measuring models’ success at developing exploits for software vulnerabilities in Firefox 147 (this evaluation was developed in collaboration with Mozilla; all vulnerabilities have been patched in Firefox 148). For each model, the left-hand bar shows how often the model (without safeguards) developed a working exploit; the right-hand bar shows how often the model had partial success. Neither of the Sonnet models could successfully develop a working exploit (both scored 0.0%); Sonnet 5 showed a slightly higher partial success rate than Sonnet 4.6. Both Sonnet models have substantially poorer cyber capabilities than Opus 4.8 and Mythos 5. For full details, see Section 3.2.4 of the Sonnet 5 System Card.
Since Sonnet 5 is somewhat stronger than its predecessor on these tasks, we’ve launched it with cyber safeguards enabled by default. These safeguards—which detect and block dangerous cyber usage in real time—are the same as those present in Claude Opus 4.7 and 4.8 (because we judged that the overall level of cybersecurity risk from Sonnet 5 was low, the safeguards are less strict than those launched with Fable 5, which block a much wider range of cybersecurity tasks).1
Our full assessment of Sonnet 5 across many safety and capability evaluations is reported in the Claude Sonnet 5 System Card.
Availability and pricing
Claude Sonnet 5 is available everywhere today at $2 per million input tokens and $10 per million output tokens2. We’ve increased rate limits across Chat, Cowork, Claude Code, and the Claude Platform3 to accommodate the higher token usage of higher effort levels; users can select whichever level makes sense for their particular project.
Changelog
Edit August 10, 2026: Sonnet 5's introductory pricing of $2 per million input tokens and $10 per million output tokens is now permanent. The standard pricing of $3 input / $15 output previously set to take effect September 1 no longer applies. The pricing references in this post have been updated accordingly.
Edit June 30, 2026: In the original version of this post, we included a cost-performance chart for the BrowseComp evaluation that was based on data from a simpler methodology that did not reflect the standard methodology we use for agentic search evaluations. This had the result of underestimating Sonnet 5's performance on the evaluation.
We have now updated the chart so that it matches the methodology that we used and discussed in the Sonnet 5 system card (which used a 10M token budget with compaction and programmatic tool calling). We have also updated the surrounding text.
Footnotes
1 Sonnet 5 is part of our Cyber Verification Program, which is available today on the native Claude Platform, the Claude Platform on AWS, and Claude in Microsoft Foundry (hosted on Azure and Anthropic), and coming soon on Claude in Google Vertex. Organizations that are already enrolled in the Cyber Verification Program automatically have the same access on Sonnet 5, with no need to reapply. Overall, we recommend Claude Opus 4.8 for cybersecurity work that requires reduced guardrails.
2 Sonnet 5 is an upgrade to Sonnet 4.6, but it uses an updated tokenizer that changes how the model processes text to improve performance (this is similar to the tokenizer change we introduced with Claude Opus 4.7). The tradeoff is that the same input can map to more tokens: roughly 1.0–1.35× depending on the content type.
3 On April 26, 2026, we raised Sonnet and Haiku rate limits at every usage tier and simplified to three tiers (Start, Build, and Scale) on the native Claude Platform. You can view your tier and current limits in the Claude Console or read the documentation to learn more.
Humanity’s Last Exam: We updated the grader model for Humanity’s Last Exam and have updated the Sonnet 4.6 score to 34.6% (no tools) and 46.8% (with tools). This is the reason the score differs from that reported in the Sonnet 4.6 launch blog.
OSWorld-Verified: We made changes to how we run the OSWorld-Verified evaluation to more accurately reflect the model’s performance in the real world, and have updated the Sonnet 4.6 score to 78.5%. This is the reason the score differs from that reported in the Sonnet 4.6 launch blog.
Related content
Developing Enterprise Frontier Safeguards with our customers
On July 30, we reported three incidents in which Claude models gained unauthorized access to real computer systems. We are conducting an in-depth analysis of both incidents, and planning to work with METR for an independent review. In the meantime, we’re sharing some of the changes we’ve made over the past month.
We’re opening a research preview of the Model Hardware Standard (MHS), a shared specification for AI agents to safely operate physical devices, to a first group of scientific research labs and advanced manufacturers.
\ No newline at end of file
diff --git a/data/artifacts/a8961211fa5b55587d3989516eaab6f50cac759e16337ac0f30077bb8ad2a025 b/data/artifacts/a8961211fa5b55587d3989516eaab6f50cac759e16337ac0f30077bb8ad2a025
new file mode 100644
index 0000000..2dd37e5
--- /dev/null
+++ b/data/artifacts/a8961211fa5b55587d3989516eaab6f50cac759e16337ac0f30077bb8ad2a025
@@ -0,0 +1 @@
+Claude Sonnet \ Anthropic
Our most agentic Sonnet yet. It brings performance that recently required larger, more expensive models to coding, tool use, and everyday work, at Sonnet’s speed and price.
Sonnet 4.6 delivers frontier performance across coding, agents, and professional work at scale. It can compress multi-day coding projects into hours and deliver production-ready solutions.
Sonnet 4.5 is the best model in the world for agents, coding, and computer use. It’s also our most accurate and detailed model for long-running tasks, with enhanced domain knowledge in coding, finance, and cybersecurity.
Sonnet 4 improves on Sonnet 3.7 across a variety of areas, especially coding. It offers frontier performance that’s practical for most AI use cases, including user-facing AI assistants and high-volume tasks.
Anyone can chat with Claude using Sonnet 5 on Claude.ai, available on web, iOS, and Android.
For developers interested in building agents, Sonnet 5 is available on the Claude Platform natively, and on Amazon Web Services, Google Cloud, and Microsoft Foundry.
Sonnet 5 is available today at $2 per million input tokens and $10 per million output tokens, with up to 90% cost savings with prompt caching and 50% cost savings with batch processing. To learn more, check out our pricing page. To get started, simply use claude-sonnet-5 via the Claude API.
For workloads that need to run in the US, US-only inference is available at 1.1x pricing for input and output tokens. Learn more.
Use cases
Sonnet 5 is a powerful, versatile model built for daily use, scaled production, and complex tasks across coding, agents, and professional workflows.
API users have fine-grained control over the model’s thinking effort. Popular use cases include:
Advanced coding
Sonnet 5 delivers exceptional coding performance across the entire software development lifecycle. From initial planning and implementation to debugging, maintenance, and large-scale refactors, it excels at reasoning through complex, multi-file codebases, producing precise implementations, and iterating with minimal back-and-forth.
Long-running agents
Sonnet 5 offers superior instruction following, tool selection, and error correction for autonomous AI workflows. It reliably handles complex, multi-step tasks that require sustained coherence and adaptive decision-making, making it an ideal backbone for customer-facing agents, internal automation, and production-grade AI systems that need to operate independently at scale.
Browser and computer use
Sonnet 5 excels in computer use capabilities, reliably handling any browser-based task from competitive analysis to procurement workflows to customer onboarding. Building on the foundation that made Sonnet the first frontier AI model to use computers, Sonnet 5 navigates digital environments with greater accuracy and reliability, enabling enterprises to automate workflows that previously required human intervention.
Enterprise workflows
Sonnet 5 handles high-volume, high-stakes professional workflows across finance, research, content, and business operations. It can analyze complex financial data, synthesize insights from internal and external sources, generate and edit documents and spreadsheets, and produce compelling written content with nuance and precision.
Benchmarks
Sonnet 5 delivers strong results across the benchmarks that matter most for real-world deployment, from reasoning and tool use to software coding, knowledge work, and more.
Trust & Safety
We’ve conducted extensive testing and evaluation of Sonnet 5, working with external experts to ensure it meets our standards for safety, security, and reliability. The accompanying system card covers safety results in depth.
Hear from our customers
Claude Sonnet 5 gives our agents a strong execution layer for multi-step software engineering work. It handles sustained coding, tool use, and debugging well across messy technical contexts, and has been especially useful for workflows where follow-through and technical grounding matter.
We handed Claude Sonnet 5 a two-part job—update Salesforce account tiers, send a launch announcement to enterprise contacts—and it finished end to end. That used to stall halfway. For day-to-day automation, it’s a no-brainer
Claude Sonnet 5 gets more done with less. Same output quality, fewer steps to get there. It refuses unsafe requests cleanly and consistently, too. At Lovable, we’re putting powerful tools in the hands of millions of builders. A model that knows when to say no is just as important as one that knows how to build.
We ran Claude Sonnet 5 against dozens of our most challenging real pull requests, and it carried each one through to a tested, verified result on its own — freeing our engineers to focus on the judgment, the decision, and the final sign-off.
I asked Claude Sonnet 5 to investigate a bug. Unprompted, it wrote a reproducing test, implemented the fix, then stashed it to confirm the bug came back without the change. All in a single pass.
With Claude Sonnet 5, agents stay on plan, follow our conventions, and ship clean multi-step changes, all at an efficient cost.
Claude Sonnet 5 is at its best on brownfield code—race conditions, hidden tests, the parts nobody wants to touch. It traces a failure to its actual root cause and ships a durable fix instead of patching the symptom.
Claude Sonnet 5 sits on the Pareto frontier for Eve’s plaintiff-law tasks. We see the clearest gains in legal research and analysis, at a price-to-performance ratio that made the choice to migrate easy.
ClickHouse agents explore live data and produce insights on the fly, so time-to-insight matters when testing new models. Claude Sonnet 5 reasons in tighter steps and gets our users to answers noticeably faster. That speed is a difference our customers feel.
At Pace, our computer-use agents run insurance workflows—submission intake, FNOL, loss runs—on the systems our operations teams already use. Claude Sonnet 5 consistently takes the right action and does it quickly, which is what real insurance work demands.
For enterprise teams managing high-volume, complex workloads, Claude Sonnet 5 represents a genuine step forward — strong performance where it counts, with the speed and cost profile that makes scaling practical. On several complex tasks it exceeds the current frontier, while delivering fast responses at low costs. For enterprises running at scale, that's a real operational win.
Claude Sonnet 5 handled the full range of coding tasks we tested it on, while resolving more issues. It's a meaningful improvement to both quality and efficiency.
Claude Sonnet 5 is a strong agentic-coding model, delivering top-tier accuracy comparable to Opus-class models and a clear step-function improvement over Sonnet 4.6. It conducts thorough explorations and sustains focus noticeably longer on complex tasks.
01 / 13
See Claude in action
Coding
What should I look for when reviewing a Pull Request for a Python web app?
We offer different models across the spectrum of speed, price, and performance. Sonnet 5 delivers top-tier intelligence with optimal efficiency for high-volume use cases. We recommend Sonnet 5 for most AI applications where you need a balance of advanced capabilities and cost-efficient performance at scale. Common use cases include customer-facing agents, production coding workflows, content generation at scale, and real-time research tasks.
Pricing depends on how you want to use Sonnet 5. To learn more, check out our pricing page.
\ No newline at end of file
diff --git a/data/artifacts/b78140937fbc6401068a829ba4b69493c544ece2c6c4e6b547a1e6deab0edd36 b/data/artifacts/b78140937fbc6401068a829ba4b69493c544ece2c6c4e6b547a1e6deab0edd36
new file mode 100644
index 0000000..6e8572a
--- /dev/null
+++ b/data/artifacts/b78140937fbc6401068a829ba4b69493c544ece2c6c4e6b547a1e6deab0edd36
@@ -0,0 +1,2 @@
+Skip to main content
The Pro plan gives you everything in a Free plan with more usage and the full set of Claude capabilities. That includes Claude Code, Claude Cowork, Claude Design, along with projects to organize your chats and documents, access to more Claude models, and Claude for Microsoft 365. You can choose a monthly or annual subscription.
The Max plan is built for people who work with Claude throughout the day and want longer, uninterrupted sessions. You get everything in Pro, plus more usage: choose from 5x or 20x the usage of Pro per 5-hour session. Max also gives you higher output limits. Both options are billed monthly. See the cards above for current pricing.
Every plan has usage limits that reset on a rolling five-hour session window, and paid plans add weekly limits on top. Your activity across Claude on web, desktop, mobile, and Claude Code all draws from the same pool. How much you can do depends on the length and complexity of your conversations, the model you choose, and the features you use, so there's no fixed message count. Free covers everyday questions. Pro gives you at least 5x more usage per 5-hour session than Free. Max gives you 5x or 20x more usage per 5-hour session than Pro. On Team plans, Standard seats give more than Pro and Premium seats give 5x more than Standard. To manage capacity and make sure all users have fair access, we may limit your usage in other ways, such as weekly and monthly caps or model and feature usage, at our discretion. When you reach a limit, you can wait for it to reset, move to a higher plan, or, on paid plans, turn on usage credits to keep working at standard API rates. You can see where you stand anytime in Settings > Usage.
Claude Code is included in all paid plans. It shares the same usage limits as the rest of your plan, so your work in the terminal and your chats draw from one pool. For heavy coding sessions, you can also switch to pay-as-you-go API credits through a Console account.
The Claude Enterprise plan is built for large businesses operating at scale. Pricing is $20 per seat per month plus usage billed at API rates, so cost scales with the models and tasks your team runs. Enterprise is billed annually and includes everything in Team plus advanced security and admin controls, including SCIM, audit logs, custom data retention, and role-based access. You can get started self-serve or contact sales for a tailored quote.
Billing and payments
Yes, annual plans are designed to cost less than monthly subscriptions. Annual subscriptions are available for Pro and Team plans. Enterprise plans are only available as annual subscriptions.
When you choose to pay annually, you're billed once up front for the year. If you're already on a monthly plan, you can switch to annual in Settings > Billing, and any unused time is credited toward your first annual invoice. See the steps for Pro and Team.
Payments are generally non-refundable, except where our Consumer Terms of Service say otherwise or where local law requires it.
Customers outside of Europe can follow this process to request a refund:
Log in, open Get help from the menu, start a message, and choose Claude Refund Request to check your eligibility.
If you subscribed through the App Store, Apple handles the refund, so you'll request it from Apple.
If you subscribed through Google Play, contact our Support team to check eligibility for an active subscription.
If you're in the European Economic Area and the United Kingdom, and within your 14-day withdrawal period, you can request a refund directly in the app.
Managing your plan
You can change your plan any time in Settings.
For individual plans, go to Settings > Billing and choose Manage subscription.
On Team plans, an Owner or Primary Owner manages this under Organization settings > Billing. See more details here.
Upgrades take effect right away, and any unused time on your current plan is credited toward the new one, so you won't pay twice for the same days.
Moving from monthly to annual works the same way, with your remaining time credited to the first annual invoice.
In some upgrades (for example Pro Annual to Max 5x Monthly), your remaining value on your old plan is larger than the cost of one billing period of the new plan. In this case, the excess will be stored as credit, which will automatically get used for future purchases.
Downgrades take effect at the end of your current billing period, and your chats, projects, and files stay with your account whichever plan you're on.
You can cancel anytime, and your plan stays active until the end of your current billing period. To avoid the next charge, cancel at least 24 hours before your renewal date.
If you signed up on the web or the desktop app, go to Settings > Billing and click Cancel.
If you subscribed through the App Store or Google Play, cancel from that app under Billing > Manage subscription, or directly in your Apple or Google Play account.
Sales-assisted Enterprise: Reach out to your Anthropic Contact or our Sales team to discuss cancellation.
Canceling doesn't delete your data. Your chats, projects, and files stay with your account, though some features aren't available on the Free plan. For Pro and Max, your account moves to the Free plan once the period ends. For step-by-step help, see how to cancel a Pro or Max plan or cancel a Team plan.
Plans & Pricing | Claude by Anthropic
\ No newline at end of file
diff --git a/data/artifacts/c862011a4ee1d1a7199fad6ca0905fa5c64f58216f7217ac1eddf37f28788433 b/data/artifacts/c862011a4ee1d1a7199fad6ca0905fa5c64f58216f7217ac1eddf37f28788433
new file mode 100644
index 0000000..eeb4ea2
--- /dev/null
+++ b/data/artifacts/c862011a4ee1d1a7199fad6ca0905fa5c64f58216f7217ac1eddf37f28788433
@@ -0,0 +1,558 @@
+
+
+
+
+
+ SWE-bench Leaderboards
+
+
+
+
+
+
+
+
+
+
+
+
+ SWE-bench
+
+
+
+
+
+
+
+
+
+
+
+ Official Leaderboards
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Compare results
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select models via the checkboxes, then click Compare results.
+
+
+
+
+
+
+
+
+
+
+
+
+
No models selected
+
+
+
+
Select at least one model using the checkboxes in the first columns, or click one of the following buttons for a pre-defined selection.
+
Quick select:
+
+
+
+
+
+
+
+
+
+
+
+
Each entry reports % Resolved, the percentage of task instances solved.
+
+
+
+ Open-weights model
+
+
+
+ Run performed or directly checked by the SWE-bench team
+
+
+
+
+
+
SWE-bench
+ 2294 instances
+
+
The original benchmark: real GitHub issues from 12 Python repositories.
The Pro plan gives you everything in a Free plan with more usage and the full set of Claude capabilities. That includes Claude Code, Claude Cowork, Claude Design, along with projects to organize your chats and documents, access to more Claude models, and Claude for Microsoft 365. You can choose a monthly or annual subscription.
The Max plan is built for people who work with Claude throughout the day and want longer, uninterrupted sessions. You get everything in Pro, plus more usage: choose from 5x or 20x the usage of Pro per 5-hour session. Max also gives you higher output limits. Both options are billed monthly. See the cards above for current pricing.
Every plan has usage limits that reset on a rolling five-hour session window, and paid plans add weekly limits on top. Your activity across Claude on web, desktop, mobile, and Claude Code all draws from the same pool. How much you can do depends on the length and complexity of your conversations, the model you choose, and the features you use, so there's no fixed message count. Free covers everyday questions. Pro gives you at least 5x more usage per 5-hour session than Free. Max gives you 5x or 20x more usage per 5-hour session than Pro. On Team plans, Standard seats give more than Pro and Premium seats give 5x more than Standard. To manage capacity and make sure all users have fair access, we may limit your usage in other ways, such as weekly and monthly caps or model and feature usage, at our discretion. When you reach a limit, you can wait for it to reset, move to a higher plan, or, on paid plans, turn on usage credits to keep working at standard API rates. You can see where you stand anytime in Settings > Usage.
Claude Code is included in all paid plans. It shares the same usage limits as the rest of your plan, so your work in the terminal and your chats draw from one pool. For heavy coding sessions, you can also switch to pay-as-you-go API credits through a Console account.
The Claude Enterprise plan is built for large businesses operating at scale. Pricing is $20 per seat per month plus usage billed at API rates, so cost scales with the models and tasks your team runs. Enterprise is billed annually and includes everything in Team plus advanced security and admin controls, including SCIM, audit logs, custom data retention, and role-based access. You can get started self-serve or contact sales for a tailored quote.
Billing and payments
Yes, annual plans are designed to cost less than monthly subscriptions. Annual subscriptions are available for Pro and Team plans. Enterprise plans are only available as annual subscriptions.
When you choose to pay annually, you're billed once up front for the year. If you're already on a monthly plan, you can switch to annual in Settings > Billing, and any unused time is credited toward your first annual invoice. See the steps for Pro and Team.
Payments are generally non-refundable, except where our Consumer Terms of Service say otherwise or where local law requires it.
Customers outside of Europe can follow this process to request a refund:
Log in, open Get help from the menu, start a message, and choose Claude Refund Request to check your eligibility.
If you subscribed through the App Store, Apple handles the refund, so you'll request it from Apple.
If you subscribed through Google Play, contact our Support team to check eligibility for an active subscription.
If you're in the European Economic Area and the United Kingdom, and within your 14-day withdrawal period, you can request a refund directly in the app.
Managing your plan
You can change your plan any time in Settings.
For individual plans, go to Settings > Billing and choose Manage subscription.
On Team plans, an Owner or Primary Owner manages this under Organization settings > Billing. See more details here.
Upgrades take effect right away, and any unused time on your current plan is credited toward the new one, so you won't pay twice for the same days.
Moving from monthly to annual works the same way, with your remaining time credited to the first annual invoice.
In some upgrades (for example Pro Annual to Max 5x Monthly), your remaining value on your old plan is larger than the cost of one billing period of the new plan. In this case, the excess will be stored as credit, which will automatically get used for future purchases.
Downgrades take effect at the end of your current billing period, and your chats, projects, and files stay with your account whichever plan you're on.
You can cancel anytime, and your plan stays active until the end of your current billing period. To avoid the next charge, cancel at least 24 hours before your renewal date.
If you signed up on the web or the desktop app, go to Settings > Billing and click Cancel.
If you subscribed through the App Store or Google Play, cancel from that app under Billing > Manage subscription, or directly in your Apple or Google Play account.
Sales-assisted Enterprise: Reach out to your Anthropic Contact or our Sales team to discuss cancellation.
Canceling doesn't delete your data. Your chats, projects, and files stay with your account, though some features aren't available on the Free plan. For Pro and Max, your account moves to the Free plan once the period ends. For step-by-step help, see how to cancel a Pro or Max plan or cancel a Team plan.
Plans & Pricing | Claude by Anthropic
\ No newline at end of file
diff --git a/data/captures/01444678f75427d4b4b108a4ea3599b69135885eaaaa3e125eaea2988541bddf.json b/data/captures/01444678f75427d4b4b108a4ea3599b69135885eaaaa3e125eaea2988541bddf.json
new file mode 100644
index 0000000..ce1d5b2
--- /dev/null
+++ b/data/captures/01444678f75427d4b4b108a4ea3599b69135885eaaaa3e125eaea2988541bddf.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":2392125,"changed":false,"content_sha256":"c862011a4ee1d1a7199fad6ca0905fa5c64f58216f7217ac1eddf37f28788433","final_url":"https://www.swebench.com/","media_type":"text/html; charset=utf-8","publisher":"SWE-bench maintainers","retrieved_at":"2026-09-13T20:53:36.147505+00:00","schema_version":1,"source_id":"swe-bench","status":"captured_unreviewed","url":"https://www.swebench.com/"}
diff --git a/data/captures/42c09fd59fada0dd8b1e77dde13e5a33d0ae696a8675d39af691941bd8c8028e.json b/data/captures/42c09fd59fada0dd8b1e77dde13e5a33d0ae696a8675d39af691941bd8c8028e.json
new file mode 100644
index 0000000..980ed39
--- /dev/null
+++ b/data/captures/42c09fd59fada0dd8b1e77dde13e5a33d0ae696a8675d39af691941bd8c8028e.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":1147265,"changed":true,"content_sha256":"b78140937fbc6401068a829ba4b69493c544ece2c6c4e6b547a1e6deab0edd36","final_url":"https://claude.com/pricing","media_type":"text/html; charset=utf-8","publisher":"Anthropic","retrieved_at":"2026-09-13T20:49:30.528597+00:00","schema_version":1,"source_id":"anthropic-pricing","status":"captured_unreviewed","url":"https://claude.com/pricing"}
diff --git a/data/captures/4a99589c743803e93ec0595344901836e6f4eb3facc2326e2d2d0109dab1c4e8.json b/data/captures/4a99589c743803e93ec0595344901836e6f4eb3facc2326e2d2d0109dab1c4e8.json
new file mode 100644
index 0000000..4f2ad54
--- /dev/null
+++ b/data/captures/4a99589c743803e93ec0595344901836e6f4eb3facc2326e2d2d0109dab1c4e8.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":2392125,"changed":true,"content_sha256":"c862011a4ee1d1a7199fad6ca0905fa5c64f58216f7217ac1eddf37f28788433","final_url":"https://www.swebench.com/","media_type":"text/html; charset=utf-8","publisher":"SWE-bench maintainers","retrieved_at":"2026-09-13T20:49:30.217693+00:00","schema_version":1,"source_id":"swe-bench","status":"captured_unreviewed","url":"https://www.swebench.com/"}
diff --git a/data/captures/5fb451cd647f5b2f235023cb61b4a8f1a45bde150c45d452782453251e9e8d2c.json b/data/captures/5fb451cd647f5b2f235023cb61b4a8f1a45bde150c45d452782453251e9e8d2c.json
new file mode 100644
index 0000000..0695ca6
--- /dev/null
+++ b/data/captures/5fb451cd647f5b2f235023cb61b4a8f1a45bde150c45d452782453251e9e8d2c.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":264372,"changed":true,"content_sha256":"87cdcea7dd7bfc4e554d3fe4e8d3377ca8a47110130537414c82cc9ac61fdd80","final_url":"https://www.anthropic.com/news/claude-sonnet-5","media_type":"text/html; charset=utf-8","publisher":"Anthropic","retrieved_at":"2026-09-13T20:52:47.496347+00:00","schema_version":1,"source_id":"anthropic-sonnet-launch","status":"captured_unreviewed","url":"https://www.anthropic.com/news/claude-sonnet-5"}
diff --git a/data/captures/74b6b3868ee49b78e44df9ab1c088042ad36bb45627edf3079c81a8942370dda.json b/data/captures/74b6b3868ee49b78e44df9ab1c088042ad36bb45627edf3079c81a8942370dda.json
new file mode 100644
index 0000000..3e21b0d
--- /dev/null
+++ b/data/captures/74b6b3868ee49b78e44df9ab1c088042ad36bb45627edf3079c81a8942370dda.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":413640,"changed":true,"content_sha256":"a8961211fa5b55587d3989516eaab6f50cac759e16337ac0f30077bb8ad2a025","final_url":"https://www.anthropic.com/claude/sonnet","media_type":"text/html; charset=utf-8","publisher":"Anthropic","retrieved_at":"2026-09-13T20:49:30.828142+00:00","schema_version":1,"source_id":"anthropic-sonnet","status":"captured_unreviewed","url":"https://www.anthropic.com/claude/sonnet"}
diff --git a/data/captures/7609eb261b17e5098147042e0dcc162c26c3d1b13e4226fd31322e0c4dda4f6b.json b/data/captures/7609eb261b17e5098147042e0dcc162c26c3d1b13e4226fd31322e0c4dda4f6b.json
new file mode 100644
index 0000000..6d84948
--- /dev/null
+++ b/data/captures/7609eb261b17e5098147042e0dcc162c26c3d1b13e4226fd31322e0c4dda4f6b.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":413640,"changed":false,"content_sha256":"a8961211fa5b55587d3989516eaab6f50cac759e16337ac0f30077bb8ad2a025","final_url":"https://www.anthropic.com/claude/sonnet","media_type":"text/html; charset=utf-8","publisher":"Anthropic","retrieved_at":"2026-09-13T20:53:38.068491+00:00","schema_version":1,"source_id":"anthropic-sonnet","status":"captured_unreviewed","url":"https://www.anthropic.com/claude/sonnet"}
diff --git a/data/captures/85d278ea018ba72b78502873729b9102a984e4169ab3281945659ca0546736fc.json b/data/captures/85d278ea018ba72b78502873729b9102a984e4169ab3281945659ca0546736fc.json
new file mode 100644
index 0000000..dfadd7a
--- /dev/null
+++ b/data/captures/85d278ea018ba72b78502873729b9102a984e4169ab3281945659ca0546736fc.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":264372,"changed":false,"content_sha256":"87cdcea7dd7bfc4e554d3fe4e8d3377ca8a47110130537414c82cc9ac61fdd80","final_url":"https://www.anthropic.com/news/claude-sonnet-5","media_type":"text/html; charset=utf-8","publisher":"Anthropic","retrieved_at":"2026-09-13T20:53:38.392839+00:00","schema_version":1,"source_id":"anthropic-sonnet-launch","status":"captured_unreviewed","url":"https://www.anthropic.com/news/claude-sonnet-5"}
diff --git a/data/captures/8d3e4d9d673d5159ecaf1e2b1042eaadb6d159405022078598990b62f8c1c061.json b/data/captures/8d3e4d9d673d5159ecaf1e2b1042eaadb6d159405022078598990b62f8c1c061.json
new file mode 100644
index 0000000..e2c3214
--- /dev/null
+++ b/data/captures/8d3e4d9d673d5159ecaf1e2b1042eaadb6d159405022078598990b62f8c1c061.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":413640,"changed":false,"content_sha256":"a8961211fa5b55587d3989516eaab6f50cac759e16337ac0f30077bb8ad2a025","final_url":"https://www.anthropic.com/claude/sonnet","media_type":"text/html; charset=utf-8","publisher":"Anthropic","retrieved_at":"2026-09-13T20:53:36.898896+00:00","schema_version":1,"source_id":"anthropic-sonnet","status":"captured_unreviewed","url":"https://www.anthropic.com/claude/sonnet"}
diff --git a/data/captures/ca6e94f8b9b2bb2102601d103fa31e1fc5000615c751358ea2dae795b040c37f.json b/data/captures/ca6e94f8b9b2bb2102601d103fa31e1fc5000615c751358ea2dae795b040c37f.json
new file mode 100644
index 0000000..299e58a
--- /dev/null
+++ b/data/captures/ca6e94f8b9b2bb2102601d103fa31e1fc5000615c751358ea2dae795b040c37f.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":2392125,"changed":false,"content_sha256":"c862011a4ee1d1a7199fad6ca0905fa5c64f58216f7217ac1eddf37f28788433","final_url":"https://www.swebench.com/","media_type":"text/html; charset=utf-8","publisher":"SWE-bench maintainers","retrieved_at":"2026-09-13T20:53:37.530744+00:00","schema_version":1,"source_id":"swe-bench","status":"captured_unreviewed","url":"https://www.swebench.com/"}
diff --git a/data/captures/d146ef54d2ca96e129b380a3d7dd8d22450c209dacf76ac2095fa88ec69001e5.json b/data/captures/d146ef54d2ca96e129b380a3d7dd8d22450c209dacf76ac2095fa88ec69001e5.json
new file mode 100644
index 0000000..e3cc0a7
--- /dev/null
+++ b/data/captures/d146ef54d2ca96e129b380a3d7dd8d22450c209dacf76ac2095fa88ec69001e5.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":1147265,"changed":true,"content_sha256":"872698ed9a658d9d5ca28a9ccb387886c695bdbe25d801027ff54baddb96bb71","final_url":"https://claude.com/pricing","media_type":"text/html; charset=utf-8","publisher":"Anthropic","retrieved_at":"2026-09-13T20:53:36.519455+00:00","schema_version":1,"source_id":"anthropic-pricing","status":"captured_unreviewed","url":"https://claude.com/pricing"}
diff --git a/data/captures/db008dc2625cd93d6836cf0848291508e304ea8c5004b9cb51d9b29a39c631a8.json b/data/captures/db008dc2625cd93d6836cf0848291508e304ea8c5004b9cb51d9b29a39c631a8.json
new file mode 100644
index 0000000..0c14bf6
--- /dev/null
+++ b/data/captures/db008dc2625cd93d6836cf0848291508e304ea8c5004b9cb51d9b29a39c631a8.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":1147341,"changed":true,"content_sha256":"f3463af48e1feeeab0a656cb7e7af15f8fabcde2853f18d0b42f905bfaea7177","final_url":"https://claude.com/pricing","media_type":"text/html; charset=utf-8","publisher":"Anthropic","retrieved_at":"2026-09-13T20:53:37.736979+00:00","schema_version":1,"source_id":"anthropic-pricing","status":"captured_unreviewed","url":"https://claude.com/pricing"}
diff --git a/data/captures/fcc84b5bb21c41b5d6588daffd41bd6d558a262480491a4e27b88d217d891f40.json b/data/captures/fcc84b5bb21c41b5d6588daffd41bd6d558a262480491a4e27b88d217d891f40.json
new file mode 100644
index 0000000..aff4bb4
--- /dev/null
+++ b/data/captures/fcc84b5bb21c41b5d6588daffd41bd6d558a262480491a4e27b88d217d891f40.json
@@ -0,0 +1 @@
+{"attempts":1,"bytes":264372,"changed":false,"content_sha256":"87cdcea7dd7bfc4e554d3fe4e8d3377ca8a47110130537414c82cc9ac61fdd80","final_url":"https://www.anthropic.com/news/claude-sonnet-5","media_type":"text/html; charset=utf-8","publisher":"Anthropic","retrieved_at":"2026-09-13T20:53:37.227224+00:00","schema_version":1,"source_id":"anthropic-sonnet-launch","status":"captured_unreviewed","url":"https://www.anthropic.com/news/claude-sonnet-5"}
diff --git a/data/ingestion-runs/2eeca4fda9a8c27201b001c541dc3c2594b52df94bd6559f337c64e03cd47b05.json b/data/ingestion-runs/2eeca4fda9a8c27201b001c541dc3c2594b52df94bd6559f337c64e03cd47b05.json
new file mode 100644
index 0000000..6f0e9cb
--- /dev/null
+++ b/data/ingestion-runs/2eeca4fda9a8c27201b001c541dc3c2594b52df94bd6559f337c64e03cd47b05.json
@@ -0,0 +1 @@
+{"observed_at":"2026-09-13T20:49:31.160878+00:00","ok":true,"sources":[{"capture_id":"4a99589c743803e93ec0595344901836e6f4eb3facc2326e2d2d0109dab1c4e8","changed":true,"ok":true,"source_id":"swe-bench"},{"capture_id":"42c09fd59fada0dd8b1e77dde13e5a33d0ae696a8675d39af691941bd8c8028e","changed":true,"ok":true,"source_id":"anthropic-pricing"},{"capture_id":"74b6b3868ee49b78e44df9ab1c088042ad36bb45627edf3079c81a8942370dda","changed":true,"ok":true,"source_id":"anthropic-sonnet"}]}
diff --git a/data/ingestion-runs/384eb09d40fd7329fdb2b8dc86acf6b34296a5c85201257366ccc6006f4cee8a.json b/data/ingestion-runs/384eb09d40fd7329fdb2b8dc86acf6b34296a5c85201257366ccc6006f4cee8a.json
new file mode 100644
index 0000000..b26be9f
--- /dev/null
+++ b/data/ingestion-runs/384eb09d40fd7329fdb2b8dc86acf6b34296a5c85201257366ccc6006f4cee8a.json
@@ -0,0 +1 @@
+{"observed_at":"2026-09-13T20:53:37.510668+00:00","ok":true,"sources":[{"capture_id":"01444678f75427d4b4b108a4ea3599b69135885eaaaa3e125eaea2988541bddf","changed":false,"ok":true,"source_id":"swe-bench"},{"capture_id":"d146ef54d2ca96e129b380a3d7dd8d22450c209dacf76ac2095fa88ec69001e5","changed":true,"ok":true,"source_id":"anthropic-pricing"},{"capture_id":"8d3e4d9d673d5159ecaf1e2b1042eaadb6d159405022078598990b62f8c1c061","changed":false,"ok":true,"source_id":"anthropic-sonnet"},{"capture_id":"fcc84b5bb21c41b5d6588daffd41bd6d558a262480491a4e27b88d217d891f40","changed":false,"ok":true,"source_id":"anthropic-sonnet-launch"}]}
diff --git a/data/ingestion-runs/67bd6d157f84a1189561da70ee1f978a2b04cd00e8085d725a7fcf60f2c8838d.json b/data/ingestion-runs/67bd6d157f84a1189561da70ee1f978a2b04cd00e8085d725a7fcf60f2c8838d.json
new file mode 100644
index 0000000..993bc6a
--- /dev/null
+++ b/data/ingestion-runs/67bd6d157f84a1189561da70ee1f978a2b04cd00e8085d725a7fcf60f2c8838d.json
@@ -0,0 +1 @@
+{"observed_at":"2026-09-13T20:53:38.706600+00:00","ok":true,"sources":[{"capture_id":"ca6e94f8b9b2bb2102601d103fa31e1fc5000615c751358ea2dae795b040c37f","changed":false,"ok":true,"source_id":"swe-bench"},{"capture_id":"db008dc2625cd93d6836cf0848291508e304ea8c5004b9cb51d9b29a39c631a8","changed":true,"ok":true,"source_id":"anthropic-pricing"},{"capture_id":"7609eb261b17e5098147042e0dcc162c26c3d1b13e4226fd31322e0c4dda4f6b","changed":false,"ok":true,"source_id":"anthropic-sonnet"},{"capture_id":"85d278ea018ba72b78502873729b9102a984e4169ab3281945659ca0546736fc","changed":false,"ok":true,"source_id":"anthropic-sonnet-launch"}]}
diff --git a/data/observations/31f3a9aaae5476b20819fd469286cb7a198a16e1d27610d288afb1922c584ad1.json b/data/observations/31f3a9aaae5476b20819fd469286cb7a198a16e1d27610d288afb1922c584ad1.json
new file mode 100644
index 0000000..cab9af0
--- /dev/null
+++ b/data/observations/31f3a9aaae5476b20819fd469286cb7a198a16e1d27610d288afb1922c584ad1.json
@@ -0,0 +1,16 @@
+{
+ "id": "31f3a9aaae5476b20819fd469286cb7a198a16e1d27610d288afb1922c584ad1",
+ "record": {
+ "schema_version": 1,
+ "title": "Primary source captured: anthropic-sonnet",
+ "claim": "Primary-source content retained for review; capture is not independent validation of its claims.",
+ "source_url": "https://www.anthropic.com/claude/sonnet",
+ "source_kind": "primary",
+ "content_sha256": "a8961211fa5b55587d3989516eaab6f50cac759e16337ac0f30077bb8ad2a025",
+ "observed_at": "2026-09-13T20:49:30.828142+00:00",
+ "event_date": "2026-09-13",
+ "status": "reported",
+ "observer": "codex",
+ "notes": "Capture ID: 74b6b3868ee49b78e44df9ab1c088042ad36bb45627edf3079c81a8942370dda"
+ }
+}
diff --git a/data/observations/510f6809bc59adb08295d2025a36c1bb40195d974e1b50563132ab99f1673aba.json b/data/observations/510f6809bc59adb08295d2025a36c1bb40195d974e1b50563132ab99f1673aba.json
new file mode 100644
index 0000000..8ad9ce1
--- /dev/null
+++ b/data/observations/510f6809bc59adb08295d2025a36c1bb40195d974e1b50563132ab99f1673aba.json
@@ -0,0 +1,16 @@
+{
+ "id": "510f6809bc59adb08295d2025a36c1bb40195d974e1b50563132ab99f1673aba",
+ "record": {
+ "schema_version": 1,
+ "title": "Source update: anthropic-pricing",
+ "claim": "Source content was captured or changed. Extracted claims require editorial review.",
+ "source_url": "https://claude.com/pricing",
+ "source_kind": "primary",
+ "content_sha256": "872698ed9a658d9d5ca28a9ccb387886c695bdbe25d801027ff54baddb96bb71",
+ "observed_at": "2026-09-13T20:53:36.519455+00:00",
+ "event_date": "2026-09-13",
+ "status": "reported",
+ "observer": "intelligence-radar/capture-v0.2",
+ "notes": "Capture ID: d146ef54d2ca96e129b380a3d7dd8d22450c209dacf76ac2095fa88ec69001e5"
+ }
+}
diff --git a/data/observations/9e50dfd4a4b47541c9fc352c2084b016af052c56b879db69a6e370c1bff0fcfc.json b/data/observations/9e50dfd4a4b47541c9fc352c2084b016af052c56b879db69a6e370c1bff0fcfc.json
new file mode 100644
index 0000000..0839336
--- /dev/null
+++ b/data/observations/9e50dfd4a4b47541c9fc352c2084b016af052c56b879db69a6e370c1bff0fcfc.json
@@ -0,0 +1,16 @@
+{
+ "id": "9e50dfd4a4b47541c9fc352c2084b016af052c56b879db69a6e370c1bff0fcfc",
+ "record": {
+ "schema_version": 1,
+ "title": "Source update: anthropic-pricing",
+ "claim": "Source content was captured or changed. Extracted claims require editorial review.",
+ "source_url": "https://claude.com/pricing",
+ "source_kind": "primary",
+ "content_sha256": "f3463af48e1feeeab0a656cb7e7af15f8fabcde2853f18d0b42f905bfaea7177",
+ "observed_at": "2026-09-13T20:53:37.736979+00:00",
+ "event_date": "2026-09-13",
+ "status": "reported",
+ "observer": "intelligence-radar/capture-v0.2",
+ "notes": "Capture ID: db008dc2625cd93d6836cf0848291508e304ea8c5004b9cb51d9b29a39c631a8"
+ }
+}
diff --git a/data/observations/bb2c9cd9dafa8644b5744df615f8a38defa8debbe03ef50ff620a0478044db5c.json b/data/observations/bb2c9cd9dafa8644b5744df615f8a38defa8debbe03ef50ff620a0478044db5c.json
new file mode 100644
index 0000000..c9499f8
--- /dev/null
+++ b/data/observations/bb2c9cd9dafa8644b5744df615f8a38defa8debbe03ef50ff620a0478044db5c.json
@@ -0,0 +1,16 @@
+{
+ "id": "bb2c9cd9dafa8644b5744df615f8a38defa8debbe03ef50ff620a0478044db5c",
+ "record": {
+ "schema_version": 1,
+ "title": "Primary source captured: anthropic-pricing",
+ "claim": "Primary-source content retained for review; capture is not independent validation of its claims.",
+ "source_url": "https://claude.com/pricing",
+ "source_kind": "primary",
+ "content_sha256": "b78140937fbc6401068a829ba4b69493c544ece2c6c4e6b547a1e6deab0edd36",
+ "observed_at": "2026-09-13T20:49:30.528597+00:00",
+ "event_date": "2026-09-13",
+ "status": "reported",
+ "observer": "codex",
+ "notes": "Capture ID: 42c09fd59fada0dd8b1e77dde13e5a33d0ae696a8675d39af691941bd8c8028e"
+ }
+}
diff --git a/data/observations/c8fdf59ceb504995bc48c3e6f99d6159bf28e7480e11c7a35daa8c0fc5caae27.json b/data/observations/c8fdf59ceb504995bc48c3e6f99d6159bf28e7480e11c7a35daa8c0fc5caae27.json
new file mode 100644
index 0000000..046467b
--- /dev/null
+++ b/data/observations/c8fdf59ceb504995bc48c3e6f99d6159bf28e7480e11c7a35daa8c0fc5caae27.json
@@ -0,0 +1,16 @@
+{
+ "id": "c8fdf59ceb504995bc48c3e6f99d6159bf28e7480e11c7a35daa8c0fc5caae27",
+ "record": {
+ "schema_version": 1,
+ "title": "Primary source captured: swe-bench",
+ "claim": "Primary-source content retained for review; capture is not independent validation of its claims.",
+ "source_url": "https://www.swebench.com/",
+ "source_kind": "primary",
+ "content_sha256": "c862011a4ee1d1a7199fad6ca0905fa5c64f58216f7217ac1eddf37f28788433",
+ "observed_at": "2026-09-13T20:49:30.217693+00:00",
+ "event_date": "2026-09-13",
+ "status": "reported",
+ "observer": "codex",
+ "notes": "Capture ID: 4a99589c743803e93ec0595344901836e6f4eb3facc2326e2d2d0109dab1c4e8"
+ }
+}
diff --git a/data/observations/eaf5ca53710c20cd9a2935e961209023b0b84058772f6e8a7bb833ac7dadf221.json b/data/observations/eaf5ca53710c20cd9a2935e961209023b0b84058772f6e8a7bb833ac7dadf221.json
new file mode 100644
index 0000000..8d0c21c
--- /dev/null
+++ b/data/observations/eaf5ca53710c20cd9a2935e961209023b0b84058772f6e8a7bb833ac7dadf221.json
@@ -0,0 +1,16 @@
+{
+ "id": "eaf5ca53710c20cd9a2935e961209023b0b84058772f6e8a7bb833ac7dadf221",
+ "record": {
+ "schema_version": 1,
+ "title": "Source update: anthropic-sonnet-launch",
+ "claim": "Source content was captured or changed. Extracted claims require editorial review.",
+ "source_url": "https://www.anthropic.com/news/claude-sonnet-5",
+ "source_kind": "primary",
+ "content_sha256": "87cdcea7dd7bfc4e554d3fe4e8d3377ca8a47110130537414c82cc9ac61fdd80",
+ "observed_at": "2026-09-13T20:52:47.496347+00:00",
+ "event_date": "2026-09-13",
+ "status": "reported",
+ "observer": "intelligence-radar/capture-v0.2",
+ "notes": "Capture ID: 5fb451cd647f5b2f235023cb61b4a8f1a45bde150c45d452782453251e9e8d2c"
+ }
+}
diff --git a/data/recommendations/024a468396eb7d73840df04271cff4c42cd14ced1b249810c399b5a565dea5f1.json b/data/recommendations/024a468396eb7d73840df04271cff4c42cd14ced1b249810c399b5a565dea5f1.json
new file mode 100644
index 0000000..2051e51
--- /dev/null
+++ b/data/recommendations/024a468396eb7d73840df04271cff4c42cd14ced1b249810c399b5a565dea5f1.json
@@ -0,0 +1 @@
+{"catalog":{"benchmarks":[{"constructs":["coding"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"swe-bench-verified","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","scale":{"maximum":100,"minimum":0},"version":"pending-verification"},{"constructs":["coding","tool_use"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"terminal-bench","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","scale":{"maximum":100,"minimum":0},"version":"pending-verification"},{"constructs":["tool_use"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"bfcl","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","scale":{"maximum":100,"minimum":0},"version":"pending-verification"},{"constructs":["web_research"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"browsecomp","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","scale":{"maximum":100,"minimum":0},"version":"pending-verification"},{"constructs":["coding","reasoning"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"scicode","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","scale":{"maximum":100,"minimum":0},"version":"pending-verification"}],"prices":[{"capture_id":"42c09fd59fada0dd8b1e77dde13e5a33d0ae696a8675d39af691941bd8c8028e","currency":"USD","observed_at":"2026-09-13T20:49:30.528597+00:00","profile_id":"claude-sonnet-5-api-default","review_status":"reviewed","reviewer":"codex","usd_per_million":{"cache_write":2.5,"cached_input":0.2,"input":2,"output":10},"valid_from":"2026-09-13","valid_until":"2026-09-13"},{"capture_id":"42c09fd59fada0dd8b1e77dde13e5a33d0ae696a8675d39af691941bd8c8028e","currency":"USD","observed_at":"2026-09-13T20:49:30.528597+00:00","profile_id":"claude-haiku-4-5-api-default","review_status":"reviewed","reviewer":"codex","usd_per_million":{"cache_write":1.25,"cached_input":0.1,"input":1,"output":5},"valid_from":"2026-09-13","valid_until":"2026-09-13"}],"profiles":[{"attributes":{"requester_access":false},"availability":{"available":true,"capture_id":"74b6b3868ee49b78e44df9ab1c088042ad36bb45627edf3079c81a8942370dda","observed_at":"2026-09-13T20:49:30.828142+00:00","review_status":"reviewed","reviewer":"codex"},"deployment":"Claude Platform/global/standard","effort":"provider-default-unspecified","harness":"direct-api-no-tools","id":"claude-sonnet-5-api-default","model":"claude-sonnet-5","provider":"Anthropic"},{"attributes":{"requester_access":false},"availability":{"available":true,"capture_id":"42c09fd59fada0dd8b1e77dde13e5a33d0ae696a8675d39af691941bd8c8028e","observed_at":"2026-09-13T20:49:30.528597+00:00","review_status":"reviewed","reviewer":"codex"},"deployment":"Claude Platform/global/standard","effort":"provider-default-unspecified","harness":"direct-api-no-tools","id":"claude-haiku-4-5-api-default","model":"claude-haiku-4-5","provider":"Anthropic"}],"results":[],"synthetic":false},"outcomes":[],"policy":{"effective_from":"2026-09-13","id":"advisor-v0.2","max_evidence_age_days":90,"minimum_outcomes":20,"redundancy_penalty":0.15,"selection_count":5,"selection_weights":{"contamination_resistance":0.05,"discriminative_power":0.12,"ecological_validity":0.15,"execution_profile_similarity":0.1,"freshness":0.1,"measurement_reliability":0.12,"reproducibility":0.06,"task_relevance":0.3},"thresholds":{"exploratory":0.6,"high_assurance":0.97,"important":0.9,"routine":0.8},"topnotch_ratio":0.9,"wilson_z":1.96},"report":{"algorithm_version":"advisor-0.2","as_of":"2026-09-13","budget":null,"confidence":"curator-assigned benchmark health; empirical bounds only for exactly matching task and profile; fitness is a proxy","efficient":null,"evidence_gaps":["Fewer than five relevant distinct measurements; ranking withheld.","No eligible profile has complete benchmark evidence.","no_evidenced_sufficient_model"],"frontier":null,"policy_id":"advisor-v0.2","profiles":[{"calibration":{"success_interval_95":null,"trials":0},"cost":{"basis":"declared workload; not a probability-derived estimate","price_observed_at":"2026-09-13T20:49:30.528597+00:00","scenario_usd":0.07},"eligible":false,"excluded_reasons":["constraint min_context not satisfied","constraint requester_access not satisfied"],"fitness_proxy":null,"id":"claude-sonnet-5-api-default","missing_measurements":["scicode","terminal-bench","browsecomp","bfcl","swe-bench-verified"],"price_status":"usable","sufficient":false},{"calibration":{"success_interval_95":null,"trials":0},"cost":{"basis":"declared workload; not a probability-derived estimate","price_observed_at":"2026-09-13T20:49:30.528597+00:00","scenario_usd":0.05},"eligible":false,"excluded_reasons":["constraint min_context not satisfied","constraint requester_access not satisfied"],"fitness_proxy":null,"id":"claude-haiku-4-5-api-default","missing_measurements":["scicode","terminal-bench","browsecomp","bfcl","swe-bench-verified"],"price_status":"usable","sufficient":false}],"ranking_scope":"eligible profiles with complete selected evidence; unknown competitors are not proven inferior","schema_version":1,"selected_benchmarks":[{"constructs":["coding","reasoning"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"scicode","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","marginal_score":0.47,"relevance":0.4,"scale":{"maximum":100,"minimum":0},"selection_score":0.47,"version":"pending-verification","weight":0.2857142857142857},{"constructs":["coding","tool_use"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"terminal-bench","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","marginal_score":0.42,"relevance":0.4,"scale":{"maximum":100,"minimum":0},"selection_score":0.47,"version":"pending-verification","weight":0.2857142857142857},{"constructs":["web_research"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"browsecomp","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","marginal_score":0.4099999999999999,"relevance":0.2,"scale":{"maximum":100,"minimum":0},"selection_score":0.4099999999999999,"version":"pending-verification","weight":0.14285714285714285},{"constructs":["tool_use"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"bfcl","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","marginal_score":0.3349999999999999,"relevance":0.2,"scale":{"maximum":100,"minimum":0},"selection_score":0.4099999999999999,"version":"pending-verification","weight":0.14285714285714285},{"constructs":["coding"],"health":{"contamination_resistance":0.5,"discriminative_power":0.5,"ecological_validity":0.5,"execution_profile_similarity":0.5,"freshness":0.5,"measurement_reliability":0.5,"reproducibility":0.5},"id":"swe-bench-verified","limitations":"Candidate instrument only. Exact benchmark version, health and profile-matched result need review. Health values are neutral curator placeholders.","marginal_score":0.3349999999999999,"relevance":0.2,"scale":{"maximum":100,"minimum":0},"selection_score":0.4099999999999999,"version":"pending-verification","weight":0.14285714285714285}],"sufficient":[],"synthetic":false,"task_key":"7679387315f6b514d3dd704f094537b3c3db42823e96631b352d66f4e5a8d706","topnotch":[]},"task":{"acceptance":"Repository acceptance tests pass and an independent reviewer accepts the change.","capabilities":{"artifact_creation":1,"coding":1,"reasoning":1,"tool_use":1,"web_research":1},"constraints":{"min_context":32000,"requester_access":true},"environment":"Proposed Claude Platform API integration; requester access and tool harness not yet evidenced.","objective":"Compare candidate API deployments for repository issue resolution.","risk":"routine","workload":{"attempts":1,"input":10000,"output":2000,"tool_usd":0.01,"verification_usd":0.02}}}
diff --git a/data/scheduler-runs/3eca8acca72fc8a1ae2cd82af72c66c3b3d7ee83b182d85d8dfb891d09d0a414.json b/data/scheduler-runs/3eca8acca72fc8a1ae2cd82af72c66c3b3d7ee83b182d85d8dfb891d09d0a414.json
new file mode 100644
index 0000000..1fa4ce9
--- /dev/null
+++ b/data/scheduler-runs/3eca8acca72fc8a1ae2cd82af72c66c3b3d7ee83b182d85d8dfb891d09d0a414.json
@@ -0,0 +1 @@
+{"digest":"digests/weekly/2026-W37.md","digest_status":"existing_preserved","finished_at":"2026-09-13T22:53:36.143928+02:00","job_id":"radar-weekly","ok":true,"period":"2026-W37"}
diff --git a/data/scheduler-runs/db0e0b6fc74a1b647b9563d25659c9063434069e8ec62d46bbb31428d34bf69e.json b/data/scheduler-runs/db0e0b6fc74a1b647b9563d25659c9063434069e8ec62d46bbb31428d34bf69e.json
new file mode 100644
index 0000000..a68b7e2
--- /dev/null
+++ b/data/scheduler-runs/db0e0b6fc74a1b647b9563d25659c9063434069e8ec62d46bbb31428d34bf69e.json
@@ -0,0 +1 @@
+{"digest":"digests/daily/2026-09-13.md","digest_status":"existing_preserved","finished_at":"2026-09-13T22:53:36.143928+02:00","job_id":"radar-daily","ok":true,"period":"2026-09-13"}
diff --git a/docs/adr/0002-reproducible-advisor-and-source-capture.md b/docs/adr/0002-reproducible-advisor-and-source-capture.md
new file mode 100644
index 0000000..235db8d
--- /dev/null
+++ b/docs/adr/0002-reproducible-advisor-and-source-capture.md
@@ -0,0 +1,47 @@
+# ADR 0002 — Reproducible advisor and source capture
+
+Accepted: 2026-09-13.
+
+The v0.2 CLI uses standard-library Python and content-addressed JSON. Source bytes
+are retained by hash. Capture receipts record requested/final URLs, publisher,
+retrieval time, media type and change detection; capture does not certify a claim.
+Changed sources create review-needed observations rather than invented extracted facts.
+
+`advisor-v0.2.json` is the executable policy. It carries forward the baseline
+selection weights, inclusive .90 Topnotch ratio and risk thresholds. It adds a
+90-day evidence freshness limit, a .15 construct-overlap redundancy penalty,
+and a minimum of 20 exact-task/profile outcomes. Earlier YAML remains an immutable
+design baseline. v0.2 is explicitly provisional and curator-driven.
+
+Five distinct nonaggregate instruments are selected greedily with construct-based
+task relevance, curator-assigned health and redundancy penalties. Raw scores use
+an explicit increasing linear scale per benchmark version. Missing scores are not
+imputed. Nonlinear metrics, Elo, lower-is-better metrics and composite indices need
+separate adapters before use. Pending benchmark versions cannot support ranking.
+
+Eligible means all declared task constraints match evidenced deployment attributes.
+Minima use `min_` keys; other constraints require exact equality. Production catalog
+attributes require curator review; available does not imply requester access.
+Fitness is comparative, not calibrated task success. Rankings cover only evidenced,
+eligible candidates, not the entire model market.
+
+Costs are explicit workload scenarios: disjoint input/cache/output buckets, tools,
+declared attempts and verification. Output includes billable reasoning. Unknown or
+conflicting prices prevent claims of cheapest. These are not estimated p50/p90s.
+
+Outcomes bind a recommendation, exact task and exact profile snapshot. Unique run
+IDs prevent duplicate evidence. Wilson bounds are reported over matching observed
+trials; Sufficient requires the lower bound above the policy threshold, with at
+least 20 trials. High-assurance also requires independent-verification assertions.
+This assumes representative independent trials; it is not a guarantee or a
+learned cross-task calibration model. Real operational outcome data is still absent.
+
+Recommendations freeze task, catalog, policy, outcomes and report. Canonical key
+ordering makes replay deterministic. Source captures and bytes remain referenced
+by immutable IDs. Replay checks algorithm version and rejects divergence.
+
+The scheduler checks the latest due daily/weekly slot in Europe/Berlin, catches up
+one slot after downtime, serializes runs, persists successes, and retries failures
+on later invocations. Each source fetch allows three bounded attempts and 5 MiB.
+Existing digest files are preserved. Receipt history distinguishes a new draft
+from an existing preserved artifact. Editorial synthesis remains a human/agent step.
diff --git a/docs/data-contracts.md b/docs/data-contracts.md
new file mode 100644
index 0000000..889e118
--- /dev/null
+++ b/docs/data-contracts.md
@@ -0,0 +1,94 @@
+# Data contracts and commands
+
+Python 3.11+; standard library only. Run commands from the checkout root.
+
+## Source collection
+
+```bash
+python3 -m intelligence_radar ingest catalog/sources.json
+python3 -m intelligence_radar validate
+make check
+```
+
+Source registry: `sources` array with unique `id`, `publisher`, and HTTPS `url`.
+The registry is trusted operator configuration; review URLs before adding sources.
+Captures live in `data/captures/.json`; original bytes are in
+`data/artifacts/`. Preserve both for replay. `ingestion-runs` records
+success/failure per source. HTML changes may be incidental, not newsworthy.
+
+Review actual source content before marking structured facts `reviewed`. Each
+result, price and availability item requires `capture_id`, timezone-aware
+`observed_at`, `review_status: reviewed`, and a named `reviewer`. Timestamps cannot
+precede the supporting capture date. An evidence ID proves linkage, not correct
+extraction. The curated initial catalog documents its missing measurement data.
+
+## Capability Advisor
+
+```bash
+python3 -m intelligence_radar advise examples/source-backed-task.json \
+ --catalog catalog/reviewed-2026-09-13.json --as-of 2026-09-13
+python3 -m intelligence_radar report
+python3 -m intelligence_radar replay
+```
+
+The source-backed example deliberately withholds winners. Its candidate benchmarks
+lack verified versions/results and its deployment access has not been established.
+This is distinct from the synthetic demonstration:
+
+```bash
+make demo-advisor
+```
+
+Task JSON: objective, acceptance, environment, risk, capability weights (0–1), hard
+constraints and workload. See examples/task.json. Input and cache token buckets are
+disjoint; output includes reasoning. Attempts are a declared scenario, never `1/fitness`.
+
+Catalog JSON: benchmark definitions (id/version/constructs/health/limitations/scale),
+exact profiles (id/model/harness/effort/provider/deployment/attributes/availability),
+results (profile_id/benchmark_id/benchmark_version/raw_score/evidence), and prices
+(profile_id/currency/valid_from/valid_until/usd_per_million/evidence). See
+examples/advisor-catalog.json for a complete synthetic shape. A synthetic catalog
+requires explicit `--synthetic` and never mixes with production evidence.
+
+For benchmark health, provide all non-task-relevance weights named in the active
+policy. Values are curator judgments, not automated validation. Duplicate IDs are
+rejected. Missing, stale, future or conflicting results remain unknown. Price
+validity is date-inclusive and was deliberately limited to the review day in the
+initial live catalog. New days need new observations.
+
+## Actual outcomes
+
+```bash
+python3 -m intelligence_radar outcome /tmp/outcome.json
+```
+
+An outcome contains recommendation_id, profile_id, unique run_id, accepted (boolean),
+independent_verification (boolean), verifier_evidence (artifact reference or notes),
+actual_usd, runtime_seconds, and completed_at (timezone-aware timestamp).
+Outcomes are immutable and run IDs are idempotent; changed duplicate runs are rejected.
+Keep the verifier artifact where its reference can be resolved. These are operator
+assertions. No actual provider execution or paid inference happens in this command.
+
+A subsequent recommendation uses matching outcomes available at its cutoff.
+A historical replay uses its original frozen set. The current baseline reports
+95% Wilson intervals, not a learned cross-task success predictor.
+
+## Scheduled operation
+
+```bash
+python3 -m intelligence_radar run-due
+python3 -m intelligence_radar health
+systemctl --user status intelligence-radar.timer
+```
+
+`ops/schedule.json` configures daily 21:30 and Sunday 20:00 Europe/Berlin.
+The shipped user timer checks every 15 minutes while this workstation's user service
+manager is running. It does not promise execution while the workstation is off.
+Each job is at most once per successful slot; failures remain retryable. Only the
+latest missed slot is caught up. Runtime state lives in ignored `.runtime/`;
+`data/scheduler-runs/` retains auditable receipts. Existing digests are preserved,
+so review the receipt's digest_status before assuming a new draft was produced.
+
+Install the reviewed units with `make install-timer`; uninstall/disable with
+`systemctl --user disable --now intelligence-radar.timer`. That command leaves
+source evidence and publications intact. No external app notifications are sent.
diff --git a/docs/evidence/source-comparison-2026-09-13.md b/docs/evidence/source-comparison-2026-09-13.md
new file mode 100644
index 0000000..5a2548e
--- /dev/null
+++ b/docs/evidence/source-comparison-2026-09-13.md
@@ -0,0 +1,28 @@
+# Capability comparison — 2026-09-13
+
+Source-backed comparison; evidence gaps remain explicit.
+
+Recommendation: `024a468396eb7d73840df04271cff4c42cd14ced1b249810c399b5a565dea5f1`
+Policy: `advisor-v0.2`
+
+| Profile | Eligible | Fitness proxy | Scenario USD | Outcome trials |
+| --- | --- | --- | --- | --- |
+| claude-sonnet-5-api-default | False | unknown | 0.0700 | 0 |
+| claude-haiku-4-5-api-default | False | unknown | 0.0500 | 0 |
+
+Frontier: undetermined
+Efficient: undetermined
+Budget: undetermined
+
+## Evidence gaps
+
+- Fewer than five relevant distinct measurements; ranking withheld.
+- No eligible profile has complete benchmark evidence.
+- no_evidenced_sufficient_model
+- claude-sonnet-5-api-default: constraint min_context not satisfied; constraint requester_access not satisfied
+- claude-sonnet-5-api-default: missing scicode, terminal-bench, browsecomp, bfcl, swe-bench-verified
+- claude-haiku-4-5-api-default: constraint min_context not satisfied; constraint requester_access not satisfied
+- claude-haiku-4-5-api-default: missing scicode, terminal-bench, browsecomp, bfcl, swe-bench-verified
+
+Cost is a declared token/tool/retry/verification scenario, not a measured success-cost distribution.
+Source review does not prove requester access or task success. See saved inputs for capture references.
diff --git a/docs/evidence/v0.2-validation.md b/docs/evidence/v0.2-validation.md
new file mode 100644
index 0000000..b690b91
--- /dev/null
+++ b/docs/evidence/v0.2-validation.md
@@ -0,0 +1,32 @@
+# v0.2 implementation evidence — 2026-09-13
+
+`make check`: 21 behavior tests cover rank/eligibility, missing or stale evidence,
+price conflicts, numeric validation, aggregate exclusion, outcome deduplication,
+empirical bounds, historical replay including JSON ordering, source integrity,
+retry/size limits, scheduler idempotence and ISO-week handling. Repository checks
+validate archive hashes, observation envelopes, retained captures and report replay.
+
+`make demo-advisor`: full synthetic ranking returns the fixture Frontier and
+Efficient, withholds Budget without outcomes, and reproduces the saved report.
+The fixture remains isolated from the operational ledger.
+
+Four primary pages were captured from SWE-bench and Anthropic. Original bytes,
+retrieval receipts and ingestion results are retained under data/. A source-backed
+comparison is in source-comparison-2026-09-13.md. It explicitly withholds winners
+because matching benchmark measurements and requester access are not evidenced.
+
+The initial price search contained stale promotion information. Inspection of
+[Anthropic's launch-page changelog](https://www.anthropic.com/news/claude-sonnet-5)
+resolved it: the August update superseded the previously scheduled price increase.
+The reviewed price observation reflects the current captured
+[pricing page](https://claude.com/pricing), with a one-day validity policy.
+
+`run-due` completed both 2026-09-13 daily and 2026-W37 weekly jobs successfully.
+Their receipts say `existing_preserved`, accurately distinguishing the imported
+historical draft from a newly generated publication. A repeat invocation performed
+no completed jobs. No external app automation or notifications were used.
+
+The user timer was installed on bnt-lap001. `systemctl --user` reports timer
+ActiveState=active, SubState=waiting; an explicit service invocation returned
+Result=success and ExecMainStatus=0. The timer checks every 15 minutes while the
+workstation user manager runs; it is not a continuously available hosted worker.
diff --git a/examples/advisor-catalog.json b/examples/advisor-catalog.json
new file mode 100644
index 0000000..8120fdd
--- /dev/null
+++ b/examples/advisor-catalog.json
@@ -0,0 +1,323 @@
+{
+ "synthetic": true,
+ "benchmarks": [
+ {
+ "id": "fixture-1",
+ "version": "1",
+ "constructs": [
+ "coding"
+ ],
+ "health": {
+ "ecological_validity": 0.8,
+ "measurement_reliability": 0.8,
+ "discriminative_power": 0.8,
+ "freshness": 0.8,
+ "execution_profile_similarity": 0.8,
+ "reproducibility": 0.8,
+ "contamination_resistance": 0.8
+ },
+ "limitations": "Synthetic instrument for behavior tests only.",
+ "scale": {
+ "minimum": 0,
+ "maximum": 100
+ }
+ },
+ {
+ "id": "fixture-2",
+ "version": "1",
+ "constructs": [
+ "tool_use"
+ ],
+ "health": {
+ "ecological_validity": 0.8,
+ "measurement_reliability": 0.8,
+ "discriminative_power": 0.8,
+ "freshness": 0.8,
+ "execution_profile_similarity": 0.8,
+ "reproducibility": 0.8,
+ "contamination_resistance": 0.8
+ },
+ "limitations": "Synthetic instrument for behavior tests only.",
+ "scale": {
+ "minimum": 0,
+ "maximum": 100
+ }
+ },
+ {
+ "id": "fixture-3",
+ "version": "1",
+ "constructs": [
+ "reasoning"
+ ],
+ "health": {
+ "ecological_validity": 0.8,
+ "measurement_reliability": 0.8,
+ "discriminative_power": 0.8,
+ "freshness": 0.8,
+ "execution_profile_similarity": 0.8,
+ "reproducibility": 0.8,
+ "contamination_resistance": 0.8
+ },
+ "limitations": "Synthetic instrument for behavior tests only.",
+ "scale": {
+ "minimum": 0,
+ "maximum": 100
+ }
+ },
+ {
+ "id": "fixture-4",
+ "version": "1",
+ "constructs": [
+ "web_research"
+ ],
+ "health": {
+ "ecological_validity": 0.8,
+ "measurement_reliability": 0.8,
+ "discriminative_power": 0.8,
+ "freshness": 0.8,
+ "execution_profile_similarity": 0.8,
+ "reproducibility": 0.8,
+ "contamination_resistance": 0.8
+ },
+ "limitations": "Synthetic instrument for behavior tests only.",
+ "scale": {
+ "minimum": 0,
+ "maximum": 100
+ }
+ },
+ {
+ "id": "fixture-5",
+ "version": "1",
+ "constructs": [
+ "artifact_creation"
+ ],
+ "health": {
+ "ecological_validity": 0.8,
+ "measurement_reliability": 0.8,
+ "discriminative_power": 0.8,
+ "freshness": 0.8,
+ "execution_profile_similarity": 0.8,
+ "reproducibility": 0.8,
+ "contamination_resistance": 0.8
+ },
+ "limitations": "Synthetic instrument for behavior tests only.",
+ "scale": {
+ "minimum": 0,
+ "maximum": 100
+ }
+ }
+ ],
+ "profiles": [
+ {
+ "id": "frontier-fixture",
+ "model": "frontier-fixture",
+ "harness": "fixture-harness-v1",
+ "effort": "default",
+ "provider": "fixture",
+ "deployment": "fixture-local",
+ "attributes": {
+ "requester_access": true,
+ "min_context": 200000
+ },
+ "availability": {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "available": true
+ }
+ },
+ {
+ "id": "efficient-fixture",
+ "model": "efficient-fixture",
+ "harness": "fixture-harness-v1",
+ "effort": "default",
+ "provider": "fixture",
+ "deployment": "fixture-local",
+ "attributes": {
+ "requester_access": true,
+ "min_context": 200000
+ },
+ "availability": {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "available": true
+ }
+ },
+ {
+ "id": "budget-fixture",
+ "model": "budget-fixture",
+ "harness": "fixture-harness-v1",
+ "effort": "default",
+ "provider": "fixture",
+ "deployment": "fixture-local",
+ "attributes": {
+ "requester_access": true,
+ "min_context": 200000
+ },
+ "availability": {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "available": true
+ }
+ }
+ ],
+ "results": [
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "frontier-fixture",
+ "benchmark_id": "fixture-1",
+ "benchmark_version": "1",
+ "raw_score": 90
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "frontier-fixture",
+ "benchmark_id": "fixture-2",
+ "benchmark_version": "1",
+ "raw_score": 90
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "frontier-fixture",
+ "benchmark_id": "fixture-3",
+ "benchmark_version": "1",
+ "raw_score": 90
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "frontier-fixture",
+ "benchmark_id": "fixture-4",
+ "benchmark_version": "1",
+ "raw_score": 90
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "frontier-fixture",
+ "benchmark_id": "fixture-5",
+ "benchmark_version": "1",
+ "raw_score": 90
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "efficient-fixture",
+ "benchmark_id": "fixture-1",
+ "benchmark_version": "1",
+ "raw_score": 85
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "efficient-fixture",
+ "benchmark_id": "fixture-2",
+ "benchmark_version": "1",
+ "raw_score": 85
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "efficient-fixture",
+ "benchmark_id": "fixture-3",
+ "benchmark_version": "1",
+ "raw_score": 85
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "efficient-fixture",
+ "benchmark_id": "fixture-4",
+ "benchmark_version": "1",
+ "raw_score": 85
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "efficient-fixture",
+ "benchmark_id": "fixture-5",
+ "benchmark_version": "1",
+ "raw_score": 85
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "budget-fixture",
+ "benchmark_id": "fixture-1",
+ "benchmark_version": "1",
+ "raw_score": 70
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "budget-fixture",
+ "benchmark_id": "fixture-2",
+ "benchmark_version": "1",
+ "raw_score": 70
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "budget-fixture",
+ "benchmark_id": "fixture-3",
+ "benchmark_version": "1",
+ "raw_score": 70
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "budget-fixture",
+ "benchmark_id": "fixture-4",
+ "benchmark_version": "1",
+ "raw_score": 70
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "budget-fixture",
+ "benchmark_id": "fixture-5",
+ "benchmark_version": "1",
+ "raw_score": 70
+ }
+ ],
+ "prices": [
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "frontier-fixture",
+ "currency": "USD",
+ "valid_from": "2026-09-13",
+ "valid_until": "2026-10-01",
+ "usd_per_million": {
+ "input": 10,
+ "output": 50
+ }
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "efficient-fixture",
+ "currency": "USD",
+ "valid_from": "2026-09-13",
+ "valid_until": "2026-10-01",
+ "usd_per_million": {
+ "input": 2,
+ "output": 10
+ }
+ },
+ {
+ "evidence_kind": "synthetic",
+ "observed_at": "2026-09-13T10:00:00Z",
+ "profile_id": "budget-fixture",
+ "currency": "USD",
+ "valid_from": "2026-09-13",
+ "valid_until": "2026-10-01",
+ "usd_per_million": {
+ "input": 0.2,
+ "output": 1.0
+ }
+ }
+ ]
+}
diff --git a/examples/source-backed-task.json b/examples/source-backed-task.json
new file mode 100644
index 0000000..fa685b0
--- /dev/null
+++ b/examples/source-backed-task.json
@@ -0,0 +1,24 @@
+{
+ "objective": "Compare candidate API deployments for repository issue resolution.",
+ "acceptance": "Repository acceptance tests pass and an independent reviewer accepts the change.",
+ "environment": "Proposed Claude Platform API integration; requester access and tool harness not yet evidenced.",
+ "risk": "routine",
+ "capabilities": {
+ "coding": 1,
+ "tool_use": 1,
+ "reasoning": 1,
+ "web_research": 1,
+ "artifact_creation": 1
+ },
+ "constraints": {
+ "requester_access": true,
+ "min_context": 32000
+ },
+ "workload": {
+ "input": 10000,
+ "output": 2000,
+ "tool_usd": 0.01,
+ "attempts": 1,
+ "verification_usd": 0.02
+ }
+}
diff --git a/examples/task.json b/examples/task.json
new file mode 100644
index 0000000..32f95e3
--- /dev/null
+++ b/examples/task.json
@@ -0,0 +1,24 @@
+{
+ "objective": "Resolve a representative repository issue with a sourced change explanation.",
+ "acceptance": "Repository acceptance tests pass and an independent reviewer accepts the change.",
+ "environment": "Fixture sandbox with shell and browser",
+ "risk": "routine",
+ "capabilities": {
+ "coding": 1,
+ "tool_use": 1,
+ "reasoning": 1,
+ "web_research": 1,
+ "artifact_creation": 1
+ },
+ "constraints": {
+ "requester_access": true,
+ "min_context": 32000
+ },
+ "workload": {
+ "input": 10000,
+ "output": 2000,
+ "tool_usd": 0.01,
+ "attempts": 1,
+ "verification_usd": 0.02
+ }
+}
diff --git a/intelligence_radar/__main__.py b/intelligence_radar/__main__.py
index cfbe30c..da66dcc 100644
--- a/intelligence_radar/__main__.py
+++ b/intelligence_radar/__main__.py
@@ -80,12 +80,8 @@ def record(root, value):
path = root / 'data/observations' / f'{ident}.json'
path.parent.mkdir(parents=True, exist_ok=True)
envelope = {'id': ident, 'record': value}
- try:
- with path.open('x') as stream:
- stream.write(json.dumps(envelope, indent=2, ensure_ascii=False) + '\n')
- except FileExistsError:
- if json.loads(path.read_text()) != envelope:
- raise ValueError('existing observation differs; refusing overwrite') from None
+ from .store import atomic
+ atomic(path, (json.dumps(envelope, indent=2, ensure_ascii=False) + '\n').encode(), immutable=True)
return ident
@@ -141,6 +137,14 @@ def check(root):
raise ValueError('archive path escapes history')
if hashlib.sha256(path.read_bytes()).hexdigest() != item['sha256']:
raise ValueError(f'archive checksum mismatch: {path}')
+ from .ingest import check_artifacts
+ from .store import all_records
+ check_artifacts(root)
+ for kind in ('recommendations', 'outcomes', 'ingestion-runs', 'scheduler-runs'):
+ all_records(root, kind)
+ from .advisor import replay
+ for ident, _ in all_records(root, 'recommendations'):
+ replay(root, ident)
return len(ledger(root))
@@ -154,9 +158,54 @@ def main(argv=None):
add.add_argument('input', type=Path)
render = commands.add_parser('digest', help='create a daily or ISO-week draft, refusing overwrite')
render.add_argument('period')
+ ingest_command = commands.add_parser('ingest', help='capture registered primary-source artifacts')
+ ingest_command.add_argument('registry', type=Path)
+ advisor_command = commands.add_parser('advise', help='save a reproducible task-specific recommendation')
+ advisor_command.add_argument('task', type=Path)
+ advisor_command.add_argument('--catalog', type=Path, required=True)
+ advisor_command.add_argument('--policy', type=Path, default=Path('policies/advisor-v0.2.json'))
+ advisor_command.add_argument('--as-of', required=True)
+ advisor_command.add_argument('--synthetic', action='store_true')
+ report_command = commands.add_parser('report', help='render a saved comparison as Markdown')
+ report_command.add_argument('id')
+ replay_command = commands.add_parser('replay', help='verify a saved recommendation using its frozen inputs')
+ replay_command.add_argument('id')
+ outcome_command = commands.add_parser('outcome', help='record an actual task execution and acceptance result')
+ outcome_command.add_argument('input', type=Path)
+ schedule_command = commands.add_parser('run-due', help='run latest due daily/weekly jobs; safe to invoke repeatedly')
+ schedule_command.add_argument('--config', type=Path, default=Path('ops/schedule.json'))
+ commands.add_parser('health', help='show durable scheduler health')
args = parser.parse_args(argv)
try:
- if args.command == 'record':
+ if args.command == 'ingest':
+ from .ingest import ingest
+ result = ingest(args.root, json.loads(args.registry.read_text()))
+ print(json.dumps(result, indent=2))
+ return 0 if result['ok'] else 1
+ elif args.command == 'advise':
+ from .advisor import recommend
+ ident, report = recommend(args.root, json.loads(args.task.read_text()),
+ json.loads(args.catalog.read_text()), json.loads(args.policy.read_text()),
+ args.as_of, args.synthetic)
+ print(json.dumps({'recommendation_id': ident, **report}, indent=2))
+ elif args.command == 'report':
+ from .advisor import render_report
+ print(render_report(args.root, args.id))
+ elif args.command == 'replay':
+ from .advisor import replay
+ print(json.dumps(replay(args.root, args.id), indent=2))
+ elif args.command == 'outcome':
+ from .advisor import outcome
+ print(outcome(args.root, json.loads(args.input.read_text())))
+ elif args.command == 'run-due':
+ from .scheduler import run_due
+ result = run_due(args.root, json.loads(args.config.read_text()))
+ print(json.dumps(result, indent=2))
+ return 0 if result['ok'] else 1
+ elif args.command == 'health':
+ path = args.root / '.runtime/scheduler.json'
+ print(path.read_text() if path.exists() else '{"status": "not_run"}')
+ elif args.command == 'record':
print(record(args.root, json.loads(args.input.read_text())))
elif args.command == 'digest':
print(draft(args.root, args.period))
diff --git a/intelligence_radar/advisor.py b/intelligence_radar/advisor.py
new file mode 100644
index 0000000..3599781
--- /dev/null
+++ b/intelligence_radar/advisor.py
@@ -0,0 +1,294 @@
+"""Deterministic task-fit ranking with explicit gaps and empirical outcome bounds."""
+from datetime import date, datetime
+import hashlib
+import json
+import math
+from pathlib import Path
+
+from .store import all_records, canonical, get, put
+
+
+def number(value, name, low=0, high=None):
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
+ raise ValueError(f'{name} must be a finite number')
+ if value < low or (high is not None and value > high):
+ raise ValueError(f'{name} outside allowed range')
+ return value
+
+
+def unique(items, label):
+ ids = [x['id'] for x in items]
+ if len(ids) != len(set(ids)):
+ raise ValueError(f'duplicate {label} IDs')
+
+
+def task_key(task):
+ # Includes acceptance, constraints, environment, workload and capability requirements.
+ return hashlib.sha256(canonical(task)).hexdigest()
+
+
+def wilson(successes, total, z):
+ p = successes / total
+ center = p + z*z / (2*total)
+ delta = z * math.sqrt(p*(1-p)/total + z*z/(4*total*total))
+ denominator = 1 + z*z/total
+ return [max(0, (center-delta)/denominator), min(1, (center+delta)/denominator)]
+
+
+def select(task, benchmarks, policy):
+ capabilities = task['capabilities']
+ if not capabilities or sum(capabilities.values()) <= 0:
+ raise ValueError('task requires positive capability weights')
+ for k, v in capabilities.items():
+ number(v, k, high=1)
+ weights = policy['selection_weights']
+ if not math.isclose(sum(weights.values()), 1):
+ raise ValueError('selection policy weights must sum to one')
+ candidates = []
+ for b in benchmarks:
+ if b.get('aggregate', False):
+ continue
+ if not b.get('version') or not b.get('limitations'):
+ raise ValueError('benchmarks require version and limitations')
+ relevance = sum(v for k, v in capabilities.items() if k in b['constructs']) / sum(capabilities.values())
+ health = b['health']
+ base = weights['task_relevance'] * relevance
+ for key, weight in weights.items():
+ if key != 'task_relevance':
+ base += weight * number(health[key], key, high=1)
+ base -= number(b.get('known_task_bias', 0), 'known_task_bias', high=1)
+ candidates.append(dict(b, relevance=relevance, selection_score=base))
+ chosen = []
+ while candidates and len(chosen) < policy['selection_count']:
+ def marginal(b):
+ overlap = max(len(set(b['constructs']) & set(s['constructs'])) /
+ max(1, len(set(b['constructs']) | set(s['constructs']))) for s in chosen) if chosen else 0
+ return b['selection_score'] - policy['redundancy_penalty'] * overlap
+ candidates.sort(key=lambda b: (-marginal(b), b['id']))
+ picked = candidates.pop(0)
+ picked['marginal_score'] = marginal(picked)
+ chosen.append(picked)
+ total = sum(b['relevance'] for b in chosen)
+ for b in chosen:
+ b['weight'] = b['relevance'] / total if total else 0
+ return chosen
+
+
+def evidence_ok(root, item, cutoff, max_age, synthetic):
+ observed = datetime.fromisoformat(item['observed_at'].replace('Z', '+00:00'))
+ if observed.tzinfo is None:
+ raise ValueError('evidence timestamps require timezone')
+ age = (cutoff - observed.date()).days
+ if age < 0 or age > max_age:
+ return False
+ if synthetic:
+ return item.get('evidence_kind') == 'synthetic'
+ if not item.get('capture_id'):
+ return False
+ capture = get(root, 'captures', item['capture_id'])
+ if not datetime.fromisoformat(capture['retrieved_at']).date() <= observed.date() <= cutoff:
+ return False
+ artifact = Path(root) / 'data/artifacts' / capture['content_sha256']
+ if hashlib.sha256(artifact.read_bytes()).hexdigest() != capture['content_sha256']:
+ raise ValueError('capture artifact integrity mismatch')
+ return item.get('review_status') == 'reviewed' and bool(item.get('reviewer'))
+
+
+def estimate_cost(task, price):
+ if price is None:
+ return None
+ workload = task['workload']
+ rates = price['usd_per_million']
+ cost = 0
+ for key in ('input', 'cached_input', 'cache_write', 'output'):
+ tokens = number(workload.get(key, 0), key)
+ if tokens and key not in rates:
+ return None
+ cost += tokens * number(rates.get(key, 0), key + ' rate') / 1_000_000
+ # Output includes billable reasoning; uncached/cache buckets are disjoint.
+ cost += number(workload['tool_usd'], 'tool_usd')
+ cost *= number(workload['attempts'], 'attempts', low=1)
+ cost += number(workload['verification_usd'], 'verification_usd')
+ return {'scenario_usd': cost, 'basis': 'declared workload; not a probability-derived estimate',
+ 'price_observed_at': price['observed_at']}
+
+
+def advise(root, task, catalog, policy, as_of, synthetic=False, saved_outcomes=None):
+ task, catalog, policy = (json.loads(canonical(x)) for x in (task, catalog, policy))
+ cutoff = date.fromisoformat(as_of)
+ number(policy['topnotch_ratio'], 'topnotch ratio', high=1)
+ number(policy['redundancy_penalty'], 'redundancy penalty', high=1)
+ number(policy['minimum_outcomes'], 'minimum outcomes', low=1)
+ number(policy['max_evidence_age_days'], 'maximum evidence age')
+ number(policy['wilson_z'], 'Wilson z', low=0.01)
+ for v in policy['thresholds'].values():
+ number(v, 'sufficiency threshold', high=1)
+ if date.fromisoformat(policy['effective_from']) > cutoff:
+ raise ValueError('policy not effective at evidence cutoff')
+ if synthetic != bool(catalog.get('synthetic', False)):
+ raise ValueError('synthetic catalogs require --synthetic and cannot mix with live catalogs')
+ if policy['selection_count'] != 5:
+ raise ValueError('advisor requires five measurement candidates')
+ for key in ('objective', 'acceptance', 'environment'):
+ if not isinstance(task[key], str) or not task[key].strip():
+ raise ValueError(f'task {key} required')
+ if task['risk'] not in policy['thresholds']:
+ raise ValueError('unknown task risk')
+ for collection in ('benchmarks', 'profiles'):
+ unique(catalog[collection], collection)
+ selected = select(task, catalog['benchmarks'], policy)
+ complete_selection = len(selected) == 5 and all(b['relevance'] > 0 and b['version'] != 'pending-verification' for b in selected)
+ gaps = [] if complete_selection else ['Fewer than five relevant distinct measurements; ranking withheld.']
+ max_age = policy['max_evidence_age_days']
+ rows = []
+ outcomes = [r for _, r in (all_records(root, 'outcomes') if saved_outcomes is None else saved_outcomes)]
+ for profile in catalog['profiles']:
+ for field in ('model', 'harness', 'effort', 'provider', 'deployment'):
+ if not profile.get(field):
+ raise ValueError(f'profile {field} required')
+ reasons = []
+ available = profile.get('availability')
+ if not available or not evidence_ok(root, available, cutoff, max_age, synthetic) or available['available'] is not True:
+ reasons.append('availability unknown, stale, or unavailable')
+ for key, required in task['constraints'].items():
+ actual = profile.get('attributes', {}).get(key)
+ if key.startswith('min_'):
+ if actual is None or number(actual, key) < number(required, key):
+ reasons.append(f'constraint {key} not satisfied')
+ elif actual != required:
+ reasons.append(f'constraint {key} not satisfied')
+ scores = []
+ for b in selected:
+ matches = [r for r in catalog['results'] if r['profile_id'] == profile['id'] and
+ r['benchmark_id'] == b['id'] and r['benchmark_version'] == b['version'] and
+ evidence_ok(root, r, cutoff, max_age, synthetic)]
+ if len(matches) != 1:
+ scores.append(None)
+ continue
+ scale = b['scale']
+ lower = number(scale['minimum'], 'scale minimum')
+ upper = number(scale['maximum'], 'scale maximum')
+ if upper <= lower:
+ raise ValueError('benchmark scale maximum must exceed minimum')
+ raw = number(matches[0]['raw_score'], 'raw score', low=lower, high=upper)
+ scores.append((raw-lower)/(upper-lower))
+ fit = sum(b['weight'] * score for b, score in zip(selected, scores)) if complete_selection and all(s is not None for s in scores) else None
+ prices = [p for p in catalog['prices'] if p['profile_id'] == profile['id'] and
+ p['currency'] == 'USD' and p['valid_from'] <= as_of <= p['valid_until'] and
+ evidence_ok(root, p, cutoff, max_age, synthetic)]
+ price = prices[0] if len(prices) == 1 else None
+ cost = estimate_cost(task, price)
+ eligible_outcomes = [o for o in outcomes if o['task_key'] == task_key(task) and
+ o['profile_hash'] == hashlib.sha256(canonical(profile)).hexdigest() and
+ o['completed_at'][:10] <= as_of and o['synthetic'] == synthetic]
+ n = len(eligible_outcomes)
+ interval = wilson(sum(o['accepted'] for o in eligible_outcomes), n, policy['wilson_z']) if n else None
+ sufficient = n >= policy['minimum_outcomes'] and interval[0] >= policy['thresholds'][task['risk']]
+ if task['risk'] == 'high_assurance':
+ sufficient = sufficient and all(o['independent_verification'] for o in eligible_outcomes)
+ rows.append({'id': profile['id'], 'eligible': not reasons, 'excluded_reasons': reasons,
+ 'fitness_proxy': fit, 'missing_measurements': [b['id'] for b, s in zip(selected, scores) if s is None],
+ 'cost': cost, 'price_status': 'usable' if price else 'missing_stale_or_conflicting',
+ 'calibration': {'trials': n, 'success_interval_95': interval},
+ 'sufficient': bool(sufficient and not reasons)})
+ ranked = sorted([r for r in rows if r['eligible'] and r['fitness_proxy'] is not None],
+ key=lambda r: (-r['fitness_proxy'], r['id']))
+ frontier = ranked[0]['id'] if ranked else None
+ topnotch = [r for r in ranked if r['fitness_proxy'] >= ranked[0]['fitness_proxy'] * policy['topnotch_ratio']]
+ sufficient = [r for r in rows if r['sufficient']]
+ def cheapest(items):
+ # Missing prices prevent a defensible claim of cheapest in the eligible set.
+ if not items or any(r['cost'] is None for r in items):
+ return None
+ return min(items, key=lambda r: (r['cost']['scenario_usd'], r['id']))['id']
+ if not ranked:
+ gaps.append('No eligible profile has complete benchmark evidence.')
+ if not sufficient:
+ gaps.append('no_evidenced_sufficient_model')
+ return {'schema_version': 1, 'algorithm_version': 'advisor-0.2', 'as_of': as_of, 'synthetic': synthetic,
+ 'task_key': task_key(task), 'policy_id': policy['id'], 'selected_benchmarks': selected,
+ 'profiles': rows, 'frontier': frontier, 'topnotch': [r['id'] for r in topnotch],
+ 'efficient': cheapest(topnotch), 'sufficient': [r['id'] for r in sufficient],
+ 'budget': cheapest(sufficient), 'evidence_gaps': gaps,
+ 'ranking_scope': 'eligible profiles with complete selected evidence; unknown competitors are not proven inferior',
+ 'confidence': 'curator-assigned benchmark health; empirical bounds only for exactly matching task and profile; fitness is a proxy'}
+
+
+def recommend(root, task, catalog, policy, as_of, synthetic=False):
+ saved = all_records(root, 'outcomes')
+ report = advise(root, task, catalog, policy, as_of, synthetic, saved)
+ snapshot = {'task': task, 'catalog': catalog, 'policy': policy,
+ 'outcomes': saved, 'report': report}
+ return put(root, 'recommendations', snapshot), report
+
+
+def _outcome(root, value):
+ recommendation = get(root, 'recommendations', value['recommendation_id'])
+ task = recommendation['task']
+ profiles = [p for p in recommendation['catalog']['profiles'] if p['id'] == value['profile_id']]
+ if len(profiles) != 1:
+ raise ValueError('outcome profile absent from recommendation')
+ for key in ('accepted', 'independent_verification'):
+ if type(value[key]) is not bool:
+ raise ValueError(f'{key} must be boolean')
+ for key in ('actual_usd', 'runtime_seconds'):
+ number(value[key], key)
+ completed = datetime.fromisoformat(value['completed_at'].replace('Z', '+00:00'))
+ if not completed.tzinfo or completed.date() < date.fromisoformat(recommendation['report']['as_of']):
+ raise ValueError('invalid outcome timestamp')
+ if not value.get('run_id') or not value.get('verifier_evidence'):
+ raise ValueError('run_id and verifier_evidence required')
+ result = dict(value, task_key=task_key(task), synthetic=recommendation['report']['synthetic'],
+ profile_hash=hashlib.sha256(canonical(profiles[0])).hexdigest())
+ for ident, existing in all_records(root, 'outcomes'):
+ if existing['run_id'] == value['run_id']:
+ if existing != result:
+ raise ValueError('run_id already recorded with different outcome')
+ return ident
+ return put(root, 'outcomes', result)
+
+
+def replay(root, ident):
+ snapshot = get(root, 'recommendations', ident)
+ report = snapshot['report']
+ if report.get('algorithm_version') != 'advisor-0.2':
+ raise ValueError('unsupported historical advisor version')
+ rebuilt = advise(root, snapshot['task'], snapshot['catalog'], snapshot['policy'],
+ report['as_of'], report['synthetic'], snapshot['outcomes'])
+ if rebuilt != report:
+ raise ValueError('recommendation replay mismatch')
+ return rebuilt
+
+
+def outcome(root, value):
+ import fcntl
+ lockpath = Path(root) / '.runtime/outcomes.lock'
+ lockpath.parent.mkdir(parents=True, exist_ok=True)
+ with lockpath.open('a') as lock:
+ fcntl.flock(lock, fcntl.LOCK_EX)
+ return _outcome(root, value)
+
+
+def render_report(root, ident):
+ report = get(root, 'recommendations', ident)['report']
+ lines = [f"# Capability comparison — {report['as_of']}", '',
+ 'Synthetic demonstration.' if report['synthetic'] else 'Source-backed comparison; evidence gaps remain explicit.', '',
+ f"Recommendation: `{ident}`", f"Policy: `{report['policy_id']}`", '',
+ '| Profile | Eligible | Fitness proxy | Scenario USD | Outcome trials |',
+ '| --- | --- | --- | --- | --- |']
+ for row in report['profiles']:
+ cost = f"{row['cost']['scenario_usd']:.4f}" if row['cost'] else 'unknown'
+ fit = f"{row['fitness_proxy']:.4f}" if row['fitness_proxy'] is not None else 'unknown'
+ lines.append(f"| {row['id']} | {row['eligible']} | {fit} | {cost} | {row['calibration']['trials']} |")
+ lines += ['', f"Frontier: {report['frontier'] or 'undetermined'}",
+ f"Efficient: {report['efficient'] or 'undetermined'}",
+ f"Budget: {report['budget'] or 'undetermined'}", '', '## Evidence gaps', '']
+ lines.extend('- '+gap for gap in report['evidence_gaps'])
+ for row in report['profiles']:
+ if row['excluded_reasons']:
+ lines.append(f"- {row['id']}: " + '; '.join(row['excluded_reasons']))
+ if row['missing_measurements']:
+ lines.append(f"- {row['id']}: missing " + ', '.join(row['missing_measurements']))
+ lines += ['', 'Cost is a declared token/tool/retry/verification scenario, not a measured success-cost distribution.',
+ 'Source review does not prove requester access or task success. See saved inputs for capture references.', '']
+ return '\n'.join(lines)
diff --git a/intelligence_radar/ingest.py b/intelligence_radar/ingest.py
new file mode 100644
index 0000000..03c3ca4
--- /dev/null
+++ b/intelligence_radar/ingest.py
@@ -0,0 +1,90 @@
+"""Bounded public HTTP capture; fetched pages are evidence, not verified claims."""
+from datetime import datetime, timezone
+import hashlib
+import json
+from pathlib import Path
+import time
+from urllib.parse import urlparse
+from urllib.request import Request, urlopen
+
+from .store import all_records, atomic, put
+
+MAX_BYTES = 5 * 1024 * 1024
+
+
+def capture(root, source, opener=urlopen, now=None):
+ url = source['url']
+ parsed = urlparse(url)
+ if parsed.scheme != 'https' or not parsed.hostname or parsed.username or parsed.password:
+ raise ValueError('sources require public HTTPS URLs without credentials')
+ if not source.get('id') or not source.get('publisher'):
+ raise ValueError('source id and publisher required')
+ timestamp = (now or datetime.now(timezone.utc)).isoformat()
+ attempts = 0
+ for attempt in range(3):
+ attempts += 1
+ try:
+ request = Request(url, headers={'User-Agent': 'IntelligenceRadar/0.2 (research source monitor)',
+ 'Accept': 'text/html,application/json,application/xml,text/plain'})
+ with opener(request, timeout=20) as response:
+ final_url = response.geturl()
+ if urlparse(final_url).scheme != 'https':
+ raise ValueError('source redirected away from HTTPS')
+ body = response.read(MAX_BYTES + 1)
+ media_type = response.headers.get('Content-Type', 'application/octet-stream')
+ break
+ except OSError:
+ if attempt == 2:
+ raise
+ time.sleep(0.25 * (2 ** attempt))
+ if len(body) > MAX_BYTES:
+ raise ValueError('source exceeds 5 MiB capture limit')
+ content_hash = hashlib.sha256(body).hexdigest()
+ artifact = Path(root) / 'data/artifacts' / content_hash
+ atomic(artifact, body, immutable=True)
+ previous = [r for _, r in all_records(root, 'captures') if r['source_id'] == source['id']]
+ previous.sort(key=lambda r: r['retrieved_at'])
+ changed = not previous or previous[-1]['content_sha256'] != content_hash
+ receipt = {'schema_version': 1, 'source_id': source['id'], 'publisher': source['publisher'],
+ 'url': url, 'final_url': final_url, 'retrieved_at': timestamp,
+ 'content_sha256': content_hash, 'media_type': media_type,
+ 'bytes': len(body), 'changed': changed, 'attempts': attempts,
+ 'status': 'captured_unreviewed'}
+ ident = put(root, 'captures', receipt)
+ if changed:
+ from .__main__ import record
+ record(root, {'schema_version': 1, 'title': f"Source update: {source['id']}",
+ 'claim': 'Source content was captured or changed. Extracted claims require editorial review.',
+ 'source_url': final_url, 'source_kind': 'primary',
+ 'content_sha256': content_hash, 'observed_at': timestamp,
+ 'event_date': timestamp[:10], 'status': 'reported',
+ 'observer': 'intelligence-radar/capture-v0.2', 'notes': f'Capture ID: {ident}'})
+ return ident, receipt
+
+
+def ingest(root, registry):
+ sources = registry['sources']
+ ids = [s['id'] for s in sources]
+ if len(set(ids)) != len(ids):
+ raise ValueError('duplicate source IDs')
+ results = []
+ for source in sources:
+ try:
+ ident, receipt = capture(root, source)
+ results.append({'source_id': source['id'], 'capture_id': ident,
+ 'ok': True, 'changed': receipt['changed']})
+ except (ValueError, OSError) as exc:
+ results.append({'source_id': source['id'], 'ok': False, 'error': str(exc)})
+ run = {'observed_at': datetime.now(timezone.utc).isoformat(), 'sources': results,
+ 'ok': all(r['ok'] for r in results)}
+ put(root, 'ingestion-runs', run)
+ return run
+
+
+def check_artifacts(root):
+ captures = all_records(root, 'captures')
+ for _, value in captures:
+ path = Path(root) / 'data/artifacts' / value['content_sha256']
+ if hashlib.sha256(path.read_bytes()).hexdigest() != value['content_sha256']:
+ raise ValueError(f'artifact integrity mismatch: {path}')
+ return len(captures)
diff --git a/intelligence_radar/scheduler.py b/intelligence_radar/scheduler.py
new file mode 100644
index 0000000..32a4480
--- /dev/null
+++ b/intelligence_radar/scheduler.py
@@ -0,0 +1,74 @@
+"""Idempotent daily/weekly source capture and digest jobs, with durable run health."""
+from datetime import datetime, timedelta
+import fcntl
+import json
+from pathlib import Path
+from zoneinfo import ZoneInfo
+
+from .ingest import ingest
+from .store import atomic, put
+
+
+def run_due(root, config, now=None, runner=ingest):
+ from .__main__ import draft
+ root = Path(root)
+ zone = ZoneInfo(config['timezone'])
+ now = now or datetime.now(zone)
+ if now.tzinfo is None:
+ raise ValueError('scheduler time requires a timezone')
+ local = now.astimezone(zone)
+ state_dir = root / '.runtime'
+ state_dir.mkdir(parents=True, exist_ok=True)
+ with (state_dir / 'scheduler.lock').open('a') as lock:
+ try:
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except BlockingIOError:
+ return {'ok': True, 'status': 'already_running', 'jobs': []}
+ state_path = state_dir / 'scheduler.json'
+ state = json.loads(state_path.read_text()) if state_path.exists() else {}
+ results = []
+ for job in config['jobs']:
+ hour, minute = (int(x) for x in job['at'].split(':'))
+ if not (0 <= hour < 24 and 0 <= minute < 60):
+ raise ValueError('invalid schedule time')
+ due = local.replace(hour=hour, minute=minute, second=0, microsecond=0)
+ if job['cadence'] == 'daily':
+ if due > local:
+ due -= timedelta(days=1)
+ period = due.date().isoformat()
+ elif job['cadence'] == 'weekly':
+ due -= timedelta(days=(due.weekday()-job['weekday']) % 7)
+ if due > local:
+ due -= timedelta(days=7)
+ year, week, _ = due.isocalendar()
+ period = f'{year}-W{week:02}'
+ else:
+ raise ValueError('unsupported cadence')
+ key = job['id'] + ':' + period
+ if state.get(key, {}).get('ok'):
+ continue
+ try:
+ registry_path = (root / job['sources']).resolve()
+ if not registry_path.is_relative_to(root.resolve()):
+ raise ValueError('source registry escapes repository')
+ registry = json.loads(registry_path.read_text())
+ result = runner(root, registry)
+ if not result['ok']:
+ raise ValueError('one or more sources failed; see ingestion-runs')
+ # Capture-only changes remain unreviewed. Do not fabricate factual events.
+ try:
+ output = draft(root, period)
+ digest_status = 'created'
+ except FileExistsError:
+ output = root / 'digests' / job['cadence'] / f'{period}.md'
+ digest_status = 'existing_preserved'
+ entry = {'ok': True, 'finished_at': local.isoformat(), 'period': period,
+ 'digest': str(output.relative_to(root)), 'digest_status': digest_status}
+ except (ValueError, OSError, KeyError) as exc:
+ entry = {'ok': False, 'finished_at': local.isoformat(), 'period': period,
+ 'error': str(exc)}
+ state[key] = entry
+ put(root, 'scheduler-runs', {'job_id': job['id'], **entry})
+ atomic(state_path, json.dumps(state, indent=2).encode() + b'\n')
+ results.append({'job_id': job['id'], **entry})
+ return {'ok': all(r['ok'] for r in results), 'status': 'checked', 'jobs': results}
diff --git a/intelligence_radar/store.py b/intelligence_radar/store.py
new file mode 100644
index 0000000..09837b6
--- /dev/null
+++ b/intelligence_radar/store.py
@@ -0,0 +1,53 @@
+"""Atomic content-addressed JSON and source artifact storage."""
+import hashlib
+import json
+import os
+from pathlib import Path
+import tempfile
+
+
+def canonical(value):
+ return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False,
+ allow_nan=False).encode('utf-8')
+
+
+def atomic(path, data, immutable=False):
+ path = Path(path)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ fd, name = tempfile.mkstemp(dir=path.parent, prefix='.write-')
+ try:
+ with os.fdopen(fd, 'wb') as stream:
+ stream.write(data)
+ stream.flush()
+ os.fsync(stream.fileno())
+ if immutable:
+ try:
+ os.link(name, path)
+ except FileExistsError:
+ if path.read_bytes() != data:
+ raise ValueError(f'immutable content mismatch: {path}') from None
+ else:
+ os.replace(name, path)
+ finally:
+ Path(name).unlink(missing_ok=True)
+
+
+def put(root, kind, value):
+ content = canonical(value)
+ ident = hashlib.sha256(content).hexdigest()
+ atomic(Path(root) / 'data' / kind / f'{ident}.json', content + b'\n', immutable=True)
+ return ident
+
+
+def get(root, kind, ident):
+ if len(ident) != 64 or any(c not in '0123456789abcdef' for c in ident):
+ raise ValueError('invalid content ID')
+ value = json.loads((Path(root) / 'data' / kind / f'{ident}.json').read_text())
+ if hashlib.sha256(canonical(value)).hexdigest() != ident:
+ raise ValueError(f'integrity mismatch: {kind}/{ident}')
+ return value
+
+
+def all_records(root, kind):
+ return [(p.stem, get(root, kind, p.stem))
+ for p in sorted((Path(root) / 'data' / kind).glob('*.json'))]
diff --git a/ops/AUTOMATION-PROMPTS.md b/ops/AUTOMATION-PROMPTS.md
index ca4e013..d842984 100644
--- a/ops/AUTOMATION-PROMPTS.md
+++ b/ops/AUTOMATION-PROMPTS.md
@@ -1,8 +1,8 @@
# Scheduling status and prompts
-Repository scheduling status: **not configured**. The source Grok archive contains
+Repository scheduling: configured local daily/weekly runner and user timer units. The source Grok archive contains
external task/schedule UUIDs; ownership and execution were not verified. They are
-historical references only. No app notifications or email have been enabled here.
+historical references only. The local timer sends no app notifications or email.
Daily operator prompt: Review the watchlist for primary-source changes since the
previous pass. Record evidence with provenance, publication/event and observation
@@ -12,4 +12,4 @@ Weekly operator prompt: Generate the ISO-week draft. Synthesize decision-relevan
changes, unresolved contradictions, frontier evidence gaps, compendium edit proposals,
and benchmark health. Link observation IDs. Do not pad an empty week with invented news.
-IR-WP-0003 tracks source adapters and an explicitly configured scheduler.
+See docs/data-contracts.md and ops/schedule.json for the implemented runner.
diff --git a/ops/CADENCE.md b/ops/CADENCE.md
index 57521e7..8299d4f 100644
--- a/ops/CADENCE.md
+++ b/ops/CADENCE.md
@@ -1,10 +1,10 @@
# Operating cadence
-Planned editorial targets; manually operated in v0.1. No scheduler is installed.
+Editorial targets; source capture and draft creation use the v0.2 scheduler. Synthesis and publication require review.
## Daily — 21:30 Europe/Berlin
-Planned job name: `intelligence-radar-daily`
+Editorial job name: `intelligence-radar-daily`
Produce a brief (800–1,400 words) and store the pattern in `digests/daily/YYYY-MM-DD.md`.
@@ -22,7 +22,7 @@ Ignore rumor without a primary. Tag confidence per item: confirmed / reported /
## Weekly — Sunday 20:00 Europe/Berlin
-Planned job name: `intelligence-radar-weekly`
+Editorial job name: `intelligence-radar-weekly`
Produce `digests/weekly/YYYY-Www.md` (ISO week).
diff --git a/ops/RUNBOOK.md b/ops/RUNBOOK.md
index 2c30cf1..0f16e40 100644
--- a/ops/RUNBOOK.md
+++ b/ops/RUNBOOK.md
@@ -38,5 +38,5 @@ Run commands at the repository root with Python 3.11+. No installation is needed
workplan files and run `make sync` for Statehub projection.
`make demo` uses a temporary directory; its synthetic observation never enters the
-repository ledger. No crawler, scheduled job, network fetch, or paid inference is
-triggered by these commands. The planned editorial cadence is in CADENCE.md.
+repository ledger. These v0.1 evidence commands do not trigger paid inference.
+For v0.2 source capture, automated jobs, advice and outcomes, see [data contracts](../docs/data-contracts.md). The planned editorial cadence is in CADENCE.md.
diff --git a/ops/schedule.json b/ops/schedule.json
new file mode 100644
index 0000000..9dfde41
--- /dev/null
+++ b/ops/schedule.json
@@ -0,0 +1,7 @@
+{
+ "timezone": "Europe/Berlin",
+ "jobs": [
+ {"id": "radar-daily", "cadence": "daily", "at": "21:30", "sources": "catalog/sources.json"},
+ {"id": "radar-weekly", "cadence": "weekly", "weekday": 6, "at": "20:00", "sources": "catalog/sources.json"}
+ ]
+}
diff --git a/ops/systemd/intelligence-radar.service b/ops/systemd/intelligence-radar.service
new file mode 100644
index 0000000..86eb0f7
--- /dev/null
+++ b/ops/systemd/intelligence-radar.service
@@ -0,0 +1,8 @@
+[Unit]
+Description=Intelligence Radar due-job runner
+After=network-online.target
+
+[Service]
+Type=oneshot
+WorkingDirectory=%h/intelligence-radar
+ExecStart=/usr/bin/python3 -m intelligence_radar run-due
diff --git a/ops/systemd/intelligence-radar.timer b/ops/systemd/intelligence-radar.timer
new file mode 100644
index 0000000..1c921e9
--- /dev/null
+++ b/ops/systemd/intelligence-radar.timer
@@ -0,0 +1,10 @@
+[Unit]
+Description=Check Intelligence Radar daily and weekly jobs
+
+[Timer]
+OnBootSec=2min
+OnUnitActiveSec=15min
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/policies/advisor-v0.2.json b/policies/advisor-v0.2.json
new file mode 100644
index 0000000..4d51e75
--- /dev/null
+++ b/policies/advisor-v0.2.json
@@ -0,0 +1,12 @@
+{
+ "id": "advisor-v0.2",
+ "effective_from": "2026-09-13",
+ "selection_count": 5,
+ "selection_weights": {"task_relevance": 0.30, "ecological_validity": 0.15, "measurement_reliability": 0.12, "discriminative_power": 0.12, "freshness": 0.10, "execution_profile_similarity": 0.10, "reproducibility": 0.06, "contamination_resistance": 0.05},
+ "redundancy_penalty": 0.15,
+ "topnotch_ratio": 0.9,
+ "thresholds": {"exploratory": 0.6, "routine": 0.8, "important": 0.9, "high_assurance": 0.97},
+ "max_evidence_age_days": 90,
+ "minimum_outcomes": 20,
+ "wilson_z": 1.96
+}
diff --git a/registry/README.md b/registry/README.md
index d2d91bc..c596186 100644
--- a/registry/README.md
+++ b/registry/README.md
@@ -1,4 +1,4 @@
# Capability registry
-The local evidence workflow is discoverable through indexes/capabilities.yaml.
-The Capability Advisor remains manual; no runtime API is advertised.
+The local source-capture, evidence and advisor workflow is discoverable through
+indexes/capabilities.yaml. Consumption is CLI and Git artifacts; no hosted API is advertised.
diff --git a/registry/capabilities/capability.infotech.intelligence-radar.md b/registry/capabilities/capability.infotech.intelligence-radar.md
index ea66795..c94fed7 100644
--- a/registry/capabilities/capability.infotech.intelligence-radar.md
+++ b/registry/capabilities/capability.infotech.intelligence-radar.md
@@ -1,21 +1,21 @@
---
id: capability.infotech.intelligence-radar
name: Intelligence Radar evidence workflow
-summary: Local provenance-preserving observation ledger and draft intelligence digests.
+summary: Local source capture, reproducible capability advice and draft intelligence digests.
owner: intelligence-radar
status: draft
domain: infotech
tags: [research, evidence, evaluation]
discovery:
intent: Support auditable intelligence research and task-specific capability advice.
- includes: [observation recording, integrity validation, draft digests, manual advisory method]
- excludes: [live model rankings, calibrated predictor, hosted API, scheduled crawling]
+ includes: [source capture, observation recording, integrity validation, draft digests, reproducible advisor, outcome records]
+ excludes: [complete market rankings, learned cross-task predictor, hosted API]
availability:
consumption_modes: [cli, informational]
current_artifacts: [intelligence_radar/, ops/RUNBOOK.md, task-router/METHOD.md]
evidence:
documentation: [README.md, SCOPE.md, ops/RUNBOOK.md]
- tests: [tests/test_workflow.py]
+ tests: [tests/test_workflow.py, tests/test_advisor.py, tests/test_ingestion_scheduler.py]
---
# Intelligence Radar evidence workflow
diff --git a/task-router/METHOD.md b/task-router/METHOD.md
index dfa2a4a..7370592 100644
--- a/task-router/METHOD.md
+++ b/task-router/METHOD.md
@@ -21,4 +21,4 @@ Use policy IDs `benchmark-selection-v0.1` and `model-ranking-v0.1`.
7. Compare optional decomposition with a monolithic baseline including coordination,
verification and failure costs. Record actual outcomes for later calibration.
-Use [TEMPLATE](TEMPLATE.md). This is a manual workflow in v0.1, not an API promise.
+Use [TEMPLATE](TEMPLATE.md). The v0.2 CLI implements selection, eligibility, ranking, scenario costs, outcomes and replay under advisor-v0.2.json. See ../docs/data-contracts.md; decomposition remains manual.
diff --git a/tests/test_advisor.py b/tests/test_advisor.py
new file mode 100644
index 0000000..b3b3a90
--- /dev/null
+++ b/tests/test_advisor.py
@@ -0,0 +1,95 @@
+import copy
+import json
+from pathlib import Path
+import tempfile
+import unittest
+
+from intelligence_radar.advisor import advise, outcome, recommend, replay
+
+REPO = Path(__file__).resolve().parents[1]
+
+
+class AdvisorTests(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.temp.cleanup)
+ self.root = Path(self.temp.name)
+ self.task = json.loads((REPO/'examples/task.json').read_text())
+ self.catalog = json.loads((REPO/'examples/advisor-catalog.json').read_text())
+ self.policy = json.loads((REPO/'policies/advisor-v0.2.json').read_text())
+
+ def report(self):
+ return advise(self.root, self.task, self.catalog, self.policy, '2026-09-13', True)
+
+ def test_rank_cost_and_no_unearned_sufficiency(self):
+ r = self.report()
+ self.assertEqual(r['frontier'], 'frontier-fixture')
+ self.assertEqual(r['efficient'], 'efficient-fixture')
+ self.assertIsNone(r['budget'])
+ self.assertEqual(len(r['selected_benchmarks']), 5)
+ self.assertAlmostEqual(sum(b['weight'] for b in r['selected_benchmarks']), 1)
+ efficient = next(x for x in r['profiles'] if x['id']=='efficient-fixture')
+ self.assertAlmostEqual(efficient['cost']['scenario_usd'], .07)
+
+ def test_missing_score_never_imputed(self):
+ self.catalog['results'].pop(0)
+ report = self.report()
+ self.assertEqual(report['frontier'], 'efficient-fixture')
+ row = next(x for x in report['profiles'] if x['id']=='frontier-fixture')
+ self.assertIsNone(row['fitness_proxy'])
+ self.assertEqual(len(row['missing_measurements']), 1)
+
+ def test_constraints_remove_highest_scoring_profile(self):
+ self.catalog['profiles'][0]['attributes']['requester_access'] = False
+ self.assertEqual(self.report()['frontier'], 'efficient-fixture')
+
+ def test_missing_or_conflicting_price_prevents_cheapest_claim(self):
+ self.catalog['prices'].pop(0)
+ self.assertIsNone(self.report()['efficient'])
+ self.catalog['prices'].append(copy.deepcopy(self.catalog['prices'][0]))
+ self.assertIsNone(self.report()['efficient'])
+
+ def test_stale_future_and_wrong_version_results(self):
+ for field, value in [('observed_at','2026-09-14T00:00:00Z'),
+ ('observed_at','2025-01-01T00:00:00Z'),
+ ('benchmark_version','other')]:
+ with self.subTest(field=field,value=value):
+ original=self.catalog['results'][0].copy()
+ self.catalog['results'][0][field]=value
+ self.assertEqual(self.report()['frontier'], 'efficient-fixture')
+ self.catalog['results'][0]=original
+
+ def test_aggregate_and_short_selection_withhold_ranking(self):
+ self.catalog['benchmarks'][0]['aggregate']=True
+ self.assertIsNone(self.report()['frontier'])
+
+ def test_calibration_duplicate_protection_and_frozen_replay(self):
+ ident, initial = recommend(self.root,self.task,self.catalog,self.policy,'2026-09-13',True)
+ value=dict(recommendation_id=ident,profile_id='budget-fixture',run_id='run-0',
+ accepted=True,independent_verification=True,verifier_evidence='synthetic test run',
+ actual_usd=.01,runtime_seconds=1,completed_at='2026-09-13T22:00:00Z')
+ first=outcome(self.root,value)
+ self.assertEqual(outcome(self.root,value),first)
+ with self.assertRaises(ValueError):
+ outcome(self.root,dict(value,accepted=False))
+ for i in range(1,20):
+ outcome(self.root,dict(value,run_id=f'run-{i}'))
+ self.assertEqual(self.report()['budget'],'budget-fixture')
+ self.assertEqual(replay(self.root,ident), initial)
+ self.task['acceptance']+=' Different acceptance test.'
+ self.assertIsNone(self.report()['budget'])
+
+ def test_synthetic_requires_explicit_opt_in(self):
+ with self.assertRaises(ValueError):
+ advise(self.root,self.task,self.catalog,self.policy,'2026-09-13')
+
+ def test_invalid_numeric_inputs_rejected(self):
+ self.catalog['results'][0]['raw_score']=float('nan')
+ with self.assertRaises(ValueError):
+ self.report()
+
+ def test_replay_is_independent_of_json_object_key_order(self):
+ self.task['constraints']['unavailable_attribute'] = True
+ self.task['capabilities']['coding'] = .31
+ ident, initial = recommend(self.root,self.task,self.catalog,self.policy,'2026-09-13',True)
+ self.assertEqual(replay(self.root,ident), initial)
diff --git a/tests/test_ingestion_scheduler.py b/tests/test_ingestion_scheduler.py
new file mode 100644
index 0000000..f2eeb4d
--- /dev/null
+++ b/tests/test_ingestion_scheduler.py
@@ -0,0 +1,67 @@
+from datetime import datetime, timezone
+import io
+import json
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from intelligence_radar.ingest import capture, check_artifacts, MAX_BYTES
+from intelligence_radar.scheduler import run_due
+from intelligence_radar.store import all_records
+
+
+class Response(io.BytesIO):
+ headers={'Content-Type':'text/plain'}
+ def geturl(self):
+ return 'https://example.invalid/source'
+
+
+class IngestionTests(unittest.TestCase):
+ def setUp(self):
+ self.temp=tempfile.TemporaryDirectory()
+ self.addCleanup(self.temp.cleanup)
+ self.root=Path(self.temp.name)
+ self.source={'id':'test','publisher':'test','url':'https://example.invalid/source'}
+
+ def test_capture_deduplication_and_integrity(self):
+ first,r=capture(self.root,self.source,opener=lambda *a,**k:Response(b'body'))
+ second,s=capture(self.root,self.source,opener=lambda *a,**k:Response(b'body'))
+ self.assertTrue(r['changed'])
+ self.assertFalse(s['changed'])
+ self.assertEqual(len(list((self.root/'data/artifacts').iterdir())),1)
+ self.assertEqual(check_artifacts(self.root),2)
+ (self.root/'data/artifacts'/r['content_sha256']).write_bytes(b'tampered')
+ with self.assertRaises(ValueError):
+ check_artifacts(self.root)
+
+ def test_retry_and_capture_limit(self):
+ with patch('intelligence_radar.ingest.time.sleep'), patch('urllib.request.urlopen'):
+ count=[0]
+ def fetch(*args,**kwargs):
+ count[0]+=1
+ if count[0]<3:
+ raise OSError('transient')
+ return Response(b'ok')
+ _,r=capture(self.root,self.source,opener=fetch)
+ self.assertEqual(r['attempts'],3)
+ with self.assertRaises(ValueError):
+ capture(self.root,self.source,opener=lambda *a,**k:Response(b'x'*(MAX_BYTES+1)))
+
+ def test_scheduler_retries_failed_job_and_preserves_existing_digest(self):
+ (self.root/'sources.json').write_text('{"sources": []}')
+ config={'timezone':'Europe/Berlin','jobs':[{'id':'daily','cadence':'daily','at':'21:30','sources':'sources.json'}]}
+ now=datetime(2026,9,13,20,tzinfo=timezone.utc)
+ failed=run_due(self.root,config,now,runner=lambda *a:{'ok':False})
+ self.assertFalse(failed['ok'])
+ passed=run_due(self.root,config,now,runner=lambda *a:{'ok':True})
+ self.assertTrue(passed['ok'])
+ self.assertEqual(len(passed['jobs']),1)
+ self.assertEqual(run_due(self.root,config,now,runner=lambda *a:self.fail('already done'))['jobs'],[])
+ self.assertEqual(len(all_records(self.root,'scheduler-runs')),2)
+
+ def test_weekly_iso_boundary_and_dst_zone(self):
+ (self.root/'sources.json').write_text('{"sources": []}')
+ config={'timezone':'Europe/Berlin','jobs':[{'id':'weekly','cadence':'weekly','weekday':6,'at':'20:00','sources':'sources.json'}]}
+ result=run_due(self.root,config,datetime(2026,1,4,19,tzinfo=timezone.utc),runner=lambda *a:{'ok':True})
+ self.assertEqual(result['jobs'][0]['period'],'2026-W01')
diff --git a/tools/demo_advisor.py b/tools/demo_advisor.py
new file mode 100644
index 0000000..735e909
--- /dev/null
+++ b/tools/demo_advisor.py
@@ -0,0 +1,22 @@
+"""Synthetic advisor and frozen-replay demonstration in temporary storage."""
+import json
+from pathlib import Path
+import sys
+import tempfile
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+from intelligence_radar.advisor import recommend, replay, render_report
+
+repo = Path(__file__).resolve().parents[1]
+with tempfile.TemporaryDirectory(prefix='radar-advisor-demo-') as directory:
+ root = Path(directory)
+ ident, report = recommend(root, json.loads((repo/'examples/task.json').read_text()),
+ json.loads((repo/'examples/advisor-catalog.json').read_text()),
+ json.loads((repo/'policies/advisor-v0.2.json').read_text()),
+ '2026-09-13', True)
+ assert replay(root, ident) == report
+ assert report['frontier'] == 'frontier-fixture'
+ assert report['efficient'] == 'efficient-fixture'
+ assert report['budget'] is None
+ print(render_report(root, ident))
+print('Advisor demo passed; synthetic records stayed in temporary storage.')
diff --git a/workplans/IR-WP-0003-verified-radar-and-advisor.md b/workplans/IR-WP-0003-verified-radar-and-advisor.md
index 0ecd65d..897ef40 100644
--- a/workplans/IR-WP-0003-verified-radar-and-advisor.md
+++ b/workplans/IR-WP-0003-verified-radar-and-advisor.md
@@ -4,7 +4,7 @@ type: workplan
title: "Verified sources and calibrated advisory capability"
domain: infotech
repo: intelligence-radar
-status: backlog
+status: finished
owner: codex
topic_slug: custodian
created: "2026-09-13"
@@ -18,7 +18,7 @@ state_hub_workstream_id: "66593995-8a1c-5c41-a36e-e4dc38675dde"
```task
id: IR-WP-0003-T01
-status: todo
+status: done
priority: medium
state_hub_task_id: "03ea42ad-0eb8-55ea-93a4-582d9310ca86"
```
@@ -29,7 +29,7 @@ Residual of IR-WP-0002: validate imported benchmark versions, prices and availab
```task
id: IR-WP-0003-T02
-status: todo
+status: done
priority: medium
state_hub_task_id: "a4809775-e432-56a0-8434-4b9164cfe012"
```
@@ -40,10 +40,21 @@ Residual of IR-WP-0002: implement five-instrument selection, eligibility gates,
```task
id: IR-WP-0003-T03
-status: todo
+status: done
priority: medium
state_hub_task_id: "123f455e-eb54-5420-91d0-608ea310e756"
```
Residual of IR-WP-0002: add source adapters, deduplication, retrieval artifacts and a configured scheduler with retry/health evidence. Confirm ownership of any external automation before reuse; repository scheduler must be explicit and testable.
+
+## Completion evidence
+
+Implemented source capture with retained artifacts and reviewed price observations;
+produced a source-backed comparison with explicit absent benchmark/access evidence.
+Implemented executable selection, eligibility, ranking, scenario economics,
+immutable outcomes, Wilson bounds and frozen historical replay. Configured and ran
+daily/weekly jobs and installed the local user timer; service exited successfully.
+Validation: 21 tests, isolated advisor demo, retained source integrity and real report
+replay. See docs/evidence/v0.2-validation.md. Broader measurement coverage, real outcome
+collection and hosted/editorial expansion are explicit residuals in IR-WP-0004.
diff --git a/workplans/IR-WP-0004-evidence-coverage-and-field-calibration.md b/workplans/IR-WP-0004-evidence-coverage-and-field-calibration.md
new file mode 100644
index 0000000..b081537
--- /dev/null
+++ b/workplans/IR-WP-0004-evidence-coverage-and-field-calibration.md
@@ -0,0 +1,57 @@
+---
+id: IR-WP-0004
+type: workplan
+title: "Expand verified evidence and calibrate against field outcomes"
+domain: infotech
+repo: intelligence-radar
+status: backlog
+owner: codex
+topic_slug: custodian
+created: "2026-09-13"
+updated: "2026-09-13"
+---
+
+# Expand verified evidence and calibrate against field outcomes
+
+Residual research and product scope from IR-WP-0003. v0.2 implements the mechanics;
+this work grows the evidence and operational reach without inventing calibration.
+
+## Build complete comparable profile evidence
+
+```task
+id: IR-WP-0004-T01
+status: todo
+priority: high
+```
+
+Review exact versions, harnesses, effort and scores for five task-relevant instruments.
+Expand beyond the two-profile dated price comparison. Preserve conflicting results
+and independently verify requester access. Acceptance: at least two eligible profiles
+with complete comparable evidence and a source-backed Frontier/Efficient comparison.
+All other imported model/benchmark claims remain research leads until reviewed.
+
+## Collect representative real task outcomes
+
+```task
+id: IR-WP-0004-T02
+status: todo
+priority: high
+```
+
+Run an explicitly budgeted sample of representative tasks with retained verifier
+artifacts. Record successes and failures, actual cost and runtime. Evaluate selection
+bias and dependence before treating Wilson bounds as predictive. Develop versioned
+cross-task calibration and measured cost/runtime intervals only when justified.
+
+## Extend publication and hosted operation
+
+```task
+id: IR-WP-0004-T03
+status: todo
+priority: medium
+```
+
+Add curated source adapters and editorial synthesis, artifact retention/export,
+reviewed source coverage, and a deployment-independent worker if continuous uptime
+is required. Add decomposition comparisons against measured monolithic outcomes.
+Keep draft creation separate from reviewed publication and any notifications.