Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name: CI
# Ruby versions this project validates continuously.
#
# The supported Ruby requirement lives in automatic.gemspec. The matrix
# below is the authoritative set validated continuously; see
# below is the authoritative set validated continuously. See
# doc/POLICY.md section 11 and doc/REQUIREMENTS.md section 20.
#
# No credential is configured here and nothing reaches an external service: the
Expand All @@ -14,8 +14,8 @@ name: CI
#
# This is the required check, and it is deliberately the minimal configuration:
# what it proves on every commit is that the framework needs only the
# runtime dependencies declared in automatic.gemspec. The all-plugins configuration is a separate,
# non-required workflow, plugins.yml.
# runtime dependencies declared in automatic.gemspec. The all-plugins
# configuration is a separate, non-required workflow, plugins.yml.

on:
push:
Expand All @@ -40,9 +40,9 @@ jobs:
fail-fast: false
matrix:
# The continuously validated versions. This matrix is authoritative for
# that set; automatic.gemspec is authoritative for the supported Ruby
# requirement. Documentation refers to these sources rather than acting as
# a second matrix.
# that set, and automatic.gemspec is authoritative for the supported
# Ruby requirement. Documentation refers to these sources rather than
# acting as a second matrix.
ruby: ['3.3', '3.4', '4.0']

steps:
Expand Down
5 changes: 3 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
# here. See doc/POLICY.md section 9.
#
# `bundle install` with no configuration installs the framework's runtime
# dependencies declared in automatic.gemspec and the development ones, and nothing below: a checkout is set
# up to run and to test the framework, not to run every plugin.
# dependencies declared in automatic.gemspec and the development ones, and
# nothing below. A checkout is set up to run and to test the framework, not to
# run every plugin.

source 'https://rubygems.org'

Expand Down
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ every time.
it collected as a Markdown document: a person reads it as it is, `grep` searches
it, Git keeps its history, and a program — a language model or an agent among
them — takes it as input without a parser or an API. That is the general case,
and it needs no account anywhere; the plugins that publish to a service are for
and it needs no account anywhere. The plugins that publish to a service are for
when a particular service is the point.

Automatic Ruby is neither host infrastructure nor a purpose-built
Expand All @@ -99,7 +99,7 @@ exactly three things:
over,
- **a loader** that finds a step by name, so that a Recipe can name it.

Everything else is a plugin. The framework is intentionally kept small; its
Everything else is a plugin. The framework is intentionally kept small. Its
responsibilities and architectural boundary are defined in
[`doc/BASIC_DESIGN.md`](doc/BASIC_DESIGN.md) and
[`doc/POLICY.md`](doc/POLICY.md).
Expand All @@ -109,7 +109,7 @@ accounts and their own files. It is not a service, and there is no notion of a
second user.

The project began in February 2012 and this is the first release since 2015. The
core, the Recipe format and the plugin contract are unchanged; what has changed
core, the Recipe format and the plugin contract are unchanged. What has changed
is that it runs on a current Ruby, installs from a current RubyGems, and ships
a plugin set every part of which still has somewhere to talk to. See
[`doc/VERSIONS`](doc/VERSIONS).
Expand Down Expand Up @@ -206,8 +206,8 @@ Two statements, and they are not the same one:
- **Continuously validated versions.** The authoritative set is the matrix in
[`.github/workflows/ci.yml`](.github/workflows/ci.yml), which represents the
supported range rather than enumerating every intermediate release. A
version's absence from the matrix means it is not verified on every commit;
it does not mean it is expected to fail.
version's absence from the matrix means it is not verified on every commit.
It does not mean it is expected to fail.

## 5. Installation

Expand All @@ -229,7 +229,8 @@ that plugin, with `gem install nokogiri` or `gem install activerecord sqlite3`.
### From a checkout

Use a checkout to try the current development version, change the source,
develop a plugin or verify changes before a release. The supported checkout setups are shown below; start with the minimal setup.
develop a plugin or verify changes before a release. The supported checkout
setups are shown below. Start with the minimal setup.

```sh
git clone https://github.com/id774/automaticruby.git
Expand Down Expand Up @@ -263,7 +264,7 @@ the checkout resolves its gems through the bundle — so a plugin's gem is added
with a group rather than with `gem install`, and a Recipe using plugins from two
groups selects both at once. The group names, which plugin needs which gem, and
a Recipe taken step by step through choosing its groups are in
[`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md); what each plugin does is in
[`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md), and what each plugin does is in
[`doc/PLUGINS.md`](doc/PLUGINS.md).

## 6. Quick start
Expand Down Expand Up @@ -420,7 +421,7 @@ self-contained contribution candidate.

No plugin here is stubbed, mocked or simulated to make a test pass. Where a
plugin's gem is not installed its spec is skipped and says which gem is
missing; where the plugin still loads, its spec covers what does not need the
missing. Where the plugin still loads, its spec covers what does not need the
service. A dead integration is never made to look alive — it is removed.

The contract, a worked example, and how to test a plugin are in
Expand Down Expand Up @@ -519,7 +520,7 @@ defence, not the boundary.)
| `~/.automatic/db` | SQLite databases the store plugins write. |
| `~/.automatic/assets` | Data files plugins read, such as the fulltext siteinfo. |

Each part is optional; where one is absent, the corresponding directory inside
Each part is optional. Where one is absent, the corresponding directory inside
the installation is used instead. `automatic scaffold` creates and seeds them
and overwrites nothing, so it is safe to run after an upgrade.

Expand Down Expand Up @@ -555,7 +556,7 @@ COVERAGE=on bundle exec rake spec
```

- A spec whose plugin needs a gem that is not installed is skipped, and says
which gem is missing. That absence is the signal; a plugin whose service no
which gem is missing. That absence is the signal. A plugin whose service no
longer exists is never stubbed into passing.
- `test/integration/` holds Recipes for exercising plugins against real
services. They are run by hand, are not part of the suite, and are never run
Expand Down Expand Up @@ -698,7 +699,7 @@ store databases — is listed in [`doc/POLICY.md`](doc/POLICY.md) section 7.

The name **Automatic Ruby** was partly inspired by David Heinemeier Hansson's
story of naming Ruby on Rails. He first chose *Rails* for the idea of putting
development "on rails"; when the obvious Rails domain names were unavailable,
development "on rails". When the obvious Rails domain names were unavailable,
`rubyonrails.com` was free. He later said he liked the result because it put
Ruby itself more prominently in the name and helped introduce Ruby to a wider
audience. See [his account of the naming](https://tim.blog/2018/06/05/the-tim-ferriss-show-transcripts-david-dhh-heinemeier-hansson/).
Expand All @@ -713,7 +714,10 @@ both, sounded natural enough, and was easy to remember. That was enough.

This repository uses `master` as its primary branch name.

The name is used solely as a technical identifier, following the long-standing convention historically used by Git. It does not express or imply any association with racism, slavery, discrimination, or any political or social ideology.
The name is used solely as a technical identifier, following the long-standing
convention historically used by Git. It does not express or imply any
association with racism, slavery, discrimination, or any political or social
ideology.

## 19. License

Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
#
# Test tasks only. This is not a build system, and the Jeweler and rcov tasks
# that used to live here have been removed; see doc/POLICY.md section 9.4.
# that used to live here have been removed. See doc/POLICY.md section 9.4.

require 'rspec/core'
require 'rspec/core/rake_task'
Expand Down
8 changes: 4 additions & 4 deletions automatic.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
# Updated:: Aug 14, 2026
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
#
# This file is maintained by hand. It was generated by Jeweler until v14.12.2;
# the generator is no longer maintained and the file is now source. See
# This file is maintained by hand. It was generated by Jeweler until v14.12.2,
# but the generator is no longer maintained and the file is now source. See
# doc/POLICY.md section 9.4.

Gem::Specification.new do |spec|
Expand Down Expand Up @@ -44,11 +44,11 @@ Gem::Specification.new do |spec|
# A lower bound only, and deliberately so. An upper bound would refuse a Ruby
# this code has every reason to work on, on the day it is released, and the
# only way to lift it would be a new release of this gem. What CI validates
# is a separate and narrower statement; see doc/REQUIREMENTS.md section 20.
# is a separate and narrower statement. See doc/REQUIREMENTS.md section 20.
spec.required_ruby_version = '>= 3.3.0'

# Shipped files. Derived from what Git sees, so that the list cannot drift
# from the repository; the Dir fallback is for building outside a checkout.
# from the repository. The Dir fallback is for building outside a checkout.
#
# --others --exclude-standard includes a file that has been created but not
# yet added, while still honouring .gitignore. Listing only --cached silently
Expand Down
31 changes: 17 additions & 14 deletions doc/AI_TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ produces something a person can read.
It assumes [`QUICKSTART.md`](QUICKSTART.md) has been followed once, so that
`automatic` runs, `~/.automatic` exists, and the habit of asking "which plugins
does this Recipe name, and what do they need" is already in place. What each
plugin does in full is [`PLUGINS.md`](PLUGINS.md) section 6; this document is
plugin does in full is [`PLUGINS.md`](PLUGINS.md) section 6. This document is
the order they go in and why.

One claim is worth stating before the first Recipe, because it is what the
Expand Down Expand Up @@ -236,7 +236,7 @@ It is worth being clear about why this is its own plugin rather than part of
what comes next. Joining a day's release notes, log lines or notifications into
one document is the same operation, and it is useful with no AI service
anywhere in the Recipe. The plugin adds no prompt and knows nothing about what
reads its result; what the joined text is *for* is decided entirely by the
reads its result. What the joined text is *for* is decided entirely by the
plugin the Recipe puts after it.

Note also what the joined item lost: its link. This is the stage the ordering
Expand Down Expand Up @@ -371,12 +371,12 @@ pipeline the two arrangements differ:
| AI filter → `FilterJoin` | 3 | Three separate texts, joined afterwards |

Both are legitimate, and they answer different questions. A digest that draws a
theme across the day's articles needs the first; a document of per-article
theme across the day's articles needs the first, and a document of per-article
summaries needs the second. The second is also the one that grows with the feed:
a run that finds forty new articles makes forty requests, each of them billed
and rate-limited. `StorePermalink` earlier in the Recipe is the main defence,
because it means each article is sent once ever; `FilterOne` ahead of the filter
is the blunt one, while a Recipe is being written.
because it means each article is sent once ever. `FilterOne` ahead of the
filter is the blunt one, while a Recipe is being written.

The point is not which order is better. It is that the difference between "one
digest" and "a list of summaries" is the position of one entry in a YAML list,
Expand All @@ -386,8 +386,9 @@ and that no plugin had to be changed or configured to express it.

Automatic Ruby uses separate service-specific AI filters rather than one
plugin with a `provider` setting. Their current catalogue and settings are
maintained in [`PLUGINS.md`](PLUGINS.md) section 6.3. Each replaces an item's description with what its service answers, so
in a Recipe they are interchangeable at the same position:
maintained in [`PLUGINS.md`](PLUGINS.md) section 6.3. Each replaces an item's
description with what its service answers, so in a Recipe they are
interchangeable at the same position:

```yaml
- module: FilterOpenAI
Expand Down Expand Up @@ -435,9 +436,10 @@ in a Recipe they are interchangeable at the same position:
```

Nothing before or after the swapped entry changes. `token`, `model` and
`prompt` are shared required settings of the AI filters; `max_tokens` exists
only for Claude because that API requires it. Model names move with the services, so take them from the provider
you are using rather than from this document, and see
`prompt` are shared required settings of the AI filters. `max_tokens` exists
only for Claude because that API requires it. Model names move with the
services, so take them from the provider you are using rather than from this
document, and see
[`PLUGINS.md`](PLUGINS.md) section 6.3 for each plugin's endpoint,
authentication and answer handling.

Expand Down Expand Up @@ -510,10 +512,11 @@ What that buys, concretely:
articles, section 3 publishes a joined document, section 4 publishes an
answer. When the last stage fails, the earlier ones still say what the run
found.
- **A failure has one address.** Nothing was fetched is `CustomFeedWeb`;
everything is a summary is `FilterFullFeed` and its siteinfo; the document is
one section is `FilterJoin`; the request was refused is the AI filter. A
single plugin that did all of it would have one log and one place to guess in.
- **A failure has one address.** When nothing was fetched, it is
`CustomFeedWeb`. When everything is a summary, it is `FilterFullFeed` and its
siteinfo. When the document is one section, it is `FilterJoin`. When the
request was refused, it is the AI filter. A single plugin that did all of it
would have one log and one place to guess in.
- **Changing behaviour is editing configuration, not code.** A different
service, a different job, one digest instead of many summaries: three edits to
one YAML file, each of them one line or one setting.
Expand Down
25 changes: 13 additions & 12 deletions doc/BASIC_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ travels through a run.
What the system is for belongs to [`REQUIREMENTS.md`](REQUIREMENTS.md). The
rules a change is held to belong to [`POLICY.md`](POLICY.md). The two public
contracts — the Recipe format and the plugin interface — are specified in
[`PLUGINS.md`](PLUGINS.md); this document explains the machinery that implements
them and does not restate them.
[`PLUGINS.md`](PLUGINS.md). This document explains the machinery that
implements them and does not restate them.

It stands on its own. Nothing in it is completed by a document kept in another
repository.
Expand All @@ -36,7 +36,7 @@ equally permanent.
requires its own libraries, at the top of its own file, so that an absent gem
is an error only for a Recipe that asked for that plugin.
- **The library never exits and never prints.** Exit status is decided by the
entry point; user-facing text is written by the entry point or logged.
entry point, and user-facing text is written by the entry point or logged.

Maintenance strength follows the architectural layer:

Expand Down Expand Up @@ -183,8 +183,8 @@ Turns a Recipe file into an object the pipeline can iterate.
- Parses the YAML **safely** — the document may contain only the plain types a
Recipe needs, so a Recipe cannot name a Ruby class to instantiate. Aliases are
permitted, because they are a legitimate way to share a block of settings
between plugins. This is a second line of defence, not the trust boundary; see
[`REQUIREMENTS.md`](REQUIREMENTS.md) section 17.
between plugins. This is a second line of defence, not the trust boundary.
See [`REQUIREMENTS.md`](REQUIREMENTS.md) section 17.
- Wraps the result in `Hashie::Mash`, which is why a plugin entry answers to
`plugin.module` and `plugin.config`, and why an absent setting reads as `nil`
rather than raising.
Expand Down Expand Up @@ -242,7 +242,7 @@ A `Logger` on standard output behind a level filter.
- `Log.level(name)` sets the threshold, from `global.log.level`.
- `Log.puts(level, message)` emits when `level` is at least the threshold.
- Levels are `info`, `warn`, `error`, `none`, compared by their position in that
list; `none` is the threshold that admits nothing.
list. `none` is the threshold that admits nothing.
- Both a `String` and a `Symbol` are accepted for a level, because plugins have
always passed both, and an unknown level is treated as `info` rather than
raising in the middle of an unattended run.
Expand Down Expand Up @@ -271,7 +271,7 @@ value has the shape it has.

- `Http.read(url)` fetches a URL and returns the body and `Http.open(url)`
yields the stream, for a caller — an HTML parser, which detects a page's
encoding for itself — that would rather not be handed a decoded string;
encoding for itself — that would rather not be handed a decoded string.
`Http.uri(url)` returns a validated URI and `Http.fetchable?(url)` answers
whether there is one.

Expand All @@ -288,7 +288,7 @@ article.

Every plugin is a class in `Automatic::Plugin`, constructed with `(config,
pipeline)` and answering `run`. The contract is specified in
[`PLUGINS.md`](PLUGINS.md) section 3; what matters to this document is how the
[`PLUGINS.md`](PLUGINS.md) section 3. What matters to this document is how the
categories divide responsibility:

| Category | Directory | Receives | Returns | Role |
Expand All @@ -307,7 +307,7 @@ that a `Filter` does not reach the network.

Replaceability is part of this boundary. A plugin is not given the same
permanence as the Recipe format or the plugin contract itself. The framework
preserves the rules that let plugins compose; it does not preserve a plugin
preserves the rules that let plugins compose. It does not preserve a plugin
whose external purpose has disappeared, and it does not move a plugin's domain
behaviour into the core merely to make that behaviour permanent.

Expand Down Expand Up @@ -337,7 +337,7 @@ or to a file. What matters to this document is where it is *not*:
framework file mentions the format. A reference to it in `lib/` would be the
same design error as a reference to any other plugin (section 3).
- **Not a second pipeline value.** Markdown is produced *from* the pipeline at
the moment the pipeline ends; it is never passed along one. The plugin returns
the moment the pipeline ends. It is never passed along one. The plugin returns
its input, so a plugin placed after it receives exactly what it would have
received without it, and Invariant 2 of [`POLICY.md`](POLICY.md) is untouched.
- **Not implicit.** Nothing appends it to a Recipe. `Pipeline.run` runs the
Expand Down Expand Up @@ -519,8 +519,9 @@ be the framework acquiring a plugin's problem. It is resolved where the choice
already exists — in the Recipe:

- `global.log.level: none` silences the log for that run, leaving standard
output carrying the document alone;
- or the plugin writes to a file it is given, and the log keeps standard output.
output carrying the document alone.
- Alternatively, the plugin writes to a file it is given, and the log keeps
standard output.

Both are stated in [`PLUGINS.md`](PLUGINS.md) section 6.7 and in
[`DEPLOYMENT.md`](DEPLOYMENT.md), which is where an operator looks.
Expand Down
Loading
Loading