cairn-pack-hello 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ # The root .gitignore drops every `data/` dir (scratch datasets); this pack's
2
+ # sample IS source, so it is re-included here.
3
+ !data/
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.5
2
+ Name: cairn-pack-hello
3
+ Version: 0.2.0
4
+ Summary: Example cairn domain pack: a data, an agentic, an ml and an operations pipeline, each behind gates.
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: cairndev>=0.1.0
8
+ Provides-Extra: all
9
+ Requires-Dist: cairndev[rag-sqlite-vec,runtime-dbos,ssh]>=0.1.0; extra == 'all'
10
+ Description-Content-Type: text/markdown
11
+
12
+ # hello-cairn: the example pack
13
+
14
+ `hello-cairn` is a domain pack: data plus a little glue, no operators. It runs on the cairn
15
+ engine and its builtin operator packs alone. Its four pipelines each do one thing a new user
16
+ recognises, and together they exercise what the engine adds around a piece of work: gates
17
+ answered by data or by a person, budgets, triggers, an agentic template, an MCP tool,
18
+ retrieval, a run hook, SSH, secrets, and an audit record.
19
+
20
+ | Pipeline | Kind | What it does |
21
+ |---|---|---|
22
+ | `profile-and-publish` | data | Profile a dataset, stop unless the profile passes, redact the summary, run a flow, stage it, ask a person, tag it, publish it, audit it. |
23
+ | `ask-the-docs` | agentic | Retrieve from the pack's docs, ask a model, measure the answer with a tool on the pack's own MCP server, remember it, check its length, ask a person. |
24
+ | `nightly-ops` | operations | Check a secret, run a report on an SSH box, carry it through a flow, propose the follow-up to a person, audit it. Nightly once enabled; a run hook writes receipts. |
25
+ | `tiny-finetune` | ml | Profile training rows, LoRA fine-tune a small model on CPU, gate on its held-out loss, tag it, ask a person, write its card. |
26
+
27
+ ## Quickstart
28
+
29
+ Run everything from the repository root: relative paths in the pipelines' defaults resolve
30
+ against the engine's working directory.
31
+
32
+ ### 1. Install, and point the engine at the example
33
+
34
+ ```bash
35
+ uv sync --extra dev --extra hello --extra rag # add --extra auth if your engine needs it
36
+ export CAIRN_PROFILE=examples/hello-cairn/profiles/hello.toml
37
+ ```
38
+
39
+ `--extra rag` is what lets the engine index the docs corpus at boot (LlamaIndex ingest);
40
+ without it ask-the-docs still runs and retrieves nothing. `auth` is optional: `cairn dev`
41
+ signs you in as dev/dev without it.
42
+
43
+ `cairndev[hello]` installs this pack as the wheel `cairn-pack-hello` plus what its pipelines
44
+ reach beyond the base install: the sqlite-vec store (retrieval and memory), asyncssh
45
+ (`remote/exec`) and DBOS (`CAIRN_RUNTIME=dbos`). Without installing it,
46
+ `export CAIRN_PACK_PATH=examples/hello-cairn` loads the same directory as a dev pack; a dev
47
+ pack declares no entry points, so the run-hook listener does not fire.
48
+
49
+ `profiles/hello.toml` is the deployment profile the example runs under. It carries
50
+ `cairn/profiles/local-dev.toml`'s bindings for the builtin operators (memory, storage, rag,
51
+ mcp), but not local-dev's `eval.delta` binding (a platform operator) or its `llm_gateway` and
52
+ `embedder` backend entries: the model and the embedder come from the engine's environment
53
+ (step 3). It adds `secrets.store` → `secrets/env` (nightly-ops' secret check is a capability
54
+ node and compiles only with a binding, which local-dev does not carry) and the `hello-calc`
55
+ MCP server the pack declares. It pins no runtime, so `CAIRN_RUNTIME=dbos` selects DBOS.
56
+
57
+ ### 2. See what will happen before anything runs
58
+
59
+ ```bash
60
+ cairn pipeline explain hello-cairn/profile-and-publish -e sandbox
61
+ cairn validate examples/hello-cairn # with CAIRN_PROFILE set as in step 1
62
+ ```
63
+
64
+ `cairn validate` needs the example's profile: under local-dev the nightly-job template does
65
+ not compile (no `secrets.store` binding).
66
+
67
+ `explain` lists every leg with the operator class or flow file it resolves to, the gate that
68
+ answers it, and the budget, series and approvers.
69
+
70
+ ### 3. Start an engine
71
+
72
+ ```bash
73
+ export OPENAI_API_KEY=sk-... # ask-the-docs' model call and embeddings
74
+ export RAG_EMBED_PROVIDER=openai # retrieval and memory embed with OpenAI
75
+ export CAIRN_RAG_AUTOINDEX=1 # index rag/docs at boot (needs the `rag` extra)
76
+ export CAIRN_MCP_SERVERS='{"hello-calc": {"transport": "stdio", "command": "python",
77
+ "args": ["examples/hello-cairn/tools/calc_server.py"]}}'
78
+ cairn dev --engine-only --port 8471 & # sqlite engine, in-process runtime, login dev/dev
79
+ cairn remote login -u dev -p dev -e http://localhost:8471
80
+ ```
81
+
82
+ Any OpenAI-compatible endpoint works in place of a key: `OPENAI_BASE_URL`, or
83
+ `LLM_GATEWAY=proxy` with `LLM_GATEWAY_URL`. The engine has no built-in mock model, and
84
+ `CAIRN_BACKEND=mock` is the telemetry backend, not a model. `profile-and-publish` and
85
+ `nightly-ops` call no model.
86
+
87
+ ### 4. Run each pipeline
88
+
89
+ **profile-and-publish**
90
+
91
+ ```bash
92
+ cairn remote launch hello-cairn/profile-and-publish --environment sandbox --watch
93
+ cairn remote advance <pipeline_id> review # the person gate
94
+ cairn remote advance <pipeline_id> promote # the operator's own gate
95
+ ```
96
+
97
+ The pipeline stops a second time at `promote`: `artifact/promote` declares
98
+ `requires_approval`, and a pipeline can add a gate but never remove one an operator declares.
99
+ `cairn remote advance` approves either kind and records the signed-in user as the approver.
100
+
101
+ The record ends `done`: `data-ok` has verdict `data`, `review` `approved`, `promote` the
102
+ tag `hello-sample@published` on the flow leg's registered artifact, and `audit` a
103
+ hash-chained row in `CAIRN_AUDIT_LOG` (default `./audit.jsonl`). `storage/put` writes under
104
+ `CAIRN_ARTIFACTS_DIR` (default `~/.cairn/artifacts`). With `-i min_rows=1000` the data gate
105
+ stops the pipeline (`gate_blocked`) before anything is written. Launch with
106
+ `--environment hello-prod` and the pack's policy
107
+ (`policies/require-review-on-publish.yaml`) stops `stage` for an approval before it writes.
108
+
109
+ **ask-the-docs**
110
+
111
+ ```bash
112
+ cairn remote launch hello-cairn/ask-the-docs --environment sandbox --watch
113
+ cairn remote advance <pipeline_id> review
114
+ cairn remote findings <pipeline_id> # hits, answer, answer_stats, remembered
115
+ ```
116
+
117
+ The `assistant` leg (`flows/assistant.md`) searches the `docs` corpus (`rag/docs`, declared
118
+ in `pack.toml` as `[requires.rag.docs]`), re-ranks, asks the model, calls `word_count` on
119
+ `tools/calc_server.py` through `mcp/call`, and writes the exchange to the `hello_answers`
120
+ memory store. `length` flags answers longer than `max_words`. An un-indexed corpus returns no
121
+ hits and the model says the docs do not cover the question. Retrieval uses sqlite-vec unless
122
+ `RAG_VECTOR_STORE=pgvector`.
123
+
124
+ **nightly-ops**
125
+
126
+ ```bash
127
+ export HELLO_REPORT_TOKEN=... # the secret the job checks for
128
+ export HELLO_SSH_KEY="$(cat ~/.ssh/id_ed25519)" # in the ENGINE's environment
129
+ CAIRN_STORAGE=sqlite:///$HOME/.cairn/dev.db \
130
+ cairn connect add ssh --name hello-ssh --set host=<box> --set user=<user> \
131
+ --set known_hosts=$HOME/.ssh/known_hosts --secret PRIVATE_KEY=HELLO_SSH_KEY
132
+ cairn remote launch hello-cairn/nightly-ops --environment sandbox --watch
133
+ ```
134
+
135
+ The connection goes into the engine's store, hence `CAIRN_STORAGE`; its secret is an env var
136
+ NAME the engine reads. The box needs `sh`, an SFTP subsystem, and `setsid` (Linux) or
137
+ `perl` (macOS): `remote/exec` detaches the job into its own session through whichever is
138
+ present (#2606). `job` (`flows/nightly-job.md`) checks the secret through the profile's
139
+ `secrets.store` binding, then runs `command` on the box and reads back `metrics.json`.
140
+ `propose` waits for a person (`monitor/propose` declares `requires_approval`;
141
+ `cairn remote advance <pipeline_id> propose`). With the wheel installed, every settled leg
142
+ appends a line to `$CAIRN_HOME/hello-cairn/receipts.jsonl` (default `~/.cairn`).
143
+
144
+ For secrets in Infisical, run the engine with
145
+ `CAIRN_PROFILE=examples/hello-cairn/profiles/hello-infisical.toml`, `uv sync --extra
146
+ infisical`, and `INFISICAL_CLIENT_ID`, `INFISICAL_CLIENT_SECRET`, `INFISICAL_PROJECT_ID`
147
+ (and `INFISICAL_HOST` for a self-hosted server). Nothing in the pipeline changes.
148
+
149
+ **tiny-finetune**
150
+
151
+ Needs the builtin train-local pack (inside `cairndev`) and its training stack on the engine's
152
+ box: `cairndev[train]` (torch, transformers, peft, datasets; CPU is enough;
153
+ there is no torch wheel for Intel macOS).
154
+
155
+ ```bash
156
+ cairn remote launch hello-cairn/tiny-finetune --environment sandbox --watch
157
+ cairn remote launch hello-cairn/tiny-finetune -i base_model=Qwen/Qwen2.5-0.5B --watch
158
+ ```
159
+
160
+ `base_model` defaults to the stand-in `sshleifer/tiny-gpt2` (seconds); Qwen2.5-0.5B trains
161
+ in minutes. `loss-ok` stops the pipeline when the held-out loss is above `target_loss`;
162
+ `promote` and `review` each wait for a person.
163
+
164
+ Until the train-local pack was merged (hello-cairn WP3), tiny-finetune was pending in two
165
+ strict xfails and one strict list entry. WP3 removed all three in the change that added the
166
+ pack: `test_tiny_finetune_is_runnable_on_the_public_core`
167
+ (`tests/core/test_example_pipeline_governance.py`) and `test_the_example_pack_validates_clean`
168
+ (`tests/cli/test_validate_example_pack.py`, under `profiles/hello.toml`) are ordinary tests,
169
+ and `tests/core/test_validate_author_gate.py` admits tiny-finetune like every other shipped
170
+ pipeline.
171
+
172
+ ### Triggers
173
+
174
+ The engine arms every declared trigger when it starts: it installs each discovered pipeline
175
+ into the `default` workspace, and each trigger becomes a row that starts its own pipeline
176
+ (ADR-0131, #2631). A trigger fires only when its declaration says `enabled: true`; to turn one
177
+ on, set that in the pipeline file and restart the engine. Out of the box only nightly-ops' cron
178
+ is on. profile-and-publish's dataset `watch` (`on_change: start`), weekly `schedule` and custom
179
+ `webhook` (`POST /triggers/hello-sample`) are `enabled: false`. Turned on, the watch makes each
180
+ tick check whether `hello-sample` gained a registered version, and starts the pipeline only
181
+ when it did. A version is registered by the `engine-check` leg, which records the checked file's content as a version of `hello-sample` (#2632): a changed sample is a new version, and the same sample adds none. With the watch on, a run that registers a new version is followed by one watch-started run on that same content; moving the registration to the job that lands the data is tracked separately. nightly-ops' `table` watch is declared but not armed: a `table` (or `s3`) watch
182
+ needs cairn-platform's governed import, so core names it unarmed. Every armed row is pinned to
183
+ start a `hello-cairn` pipeline.
184
+
185
+ ### Events
186
+
187
+ `profile-and-publish`'s `publish` leg declares `emits: dataset.ready`: when it completes,
188
+ the bus carries that topic with the leg's output as the payload. A leg declared
189
+ `trigger: on:dataset.ready` (in any pipeline) waits for it; `trigger:` also takes `manual`
190
+ and `schedule:<cron>` (#2605). On the Postgres bus the event is durable and replayed to a
191
+ leg that starts waiting later; on the in-process bus only a leg already waiting sees it.
192
+
193
+ A whole pipeline can start on a topic too: `ask-the-docs` declares a `kind: event` trigger on
194
+ `dataset.ready` (`enabled: false`). Turned on, each published `dataset.ready` starts one
195
+ ask-the-docs run (ADR-0131 D4). The listener runs on the engine's leader, so a message
196
+ published while no leader is subscribed starts nothing, on either bus. A pipeline may not
197
+ start on a topic its own legs emit.
198
+
199
+ ### How it runs inside the engine
200
+
201
+ [docs/golden-paths/hello.md](../../docs/golden-paths/hello.md) traces each pipeline from the
202
+ start door to its legs, names where governance fires, and lists the proof for each feature.
203
+
204
+ ### Seeded demo data
205
+
206
+ `cairn dev --seed` and `cairn demo` seed three records of profile-and-publish
207
+ (`cairn/core/demo_seed.py`). The seeder takes the steps from the loaded pipeline's plan, so
208
+ each record carries every step in plan order (`redact`, `promote` and `audit` included):
209
+ the published record has all of them done, the blocked one stops at `data-ok`, and the
210
+ waiting one stops at `review` with the later steps pending (#2608).
211
+
212
+ ## Files
213
+
214
+ ```
215
+ pack.toml manifest (kind = domain): the docs corpus, the MCP server
216
+ __init__.py the one-line shim the entry point targets
217
+ pipelines/*.yaml the four pipelines (the runnable surface)
218
+ flows/engine-check.yaml data flow: one noop stage
219
+ flows/summarise.yaml data flow: noop + noop.slow
220
+ flows/lora.yaml ml flow: train.lora
221
+ flows/assistant.md agentic template of ask-the-docs
222
+ flows/nightly-job.md agentic template of nightly-ops (no model)
223
+ rag/docs/*.txt the docs corpus
224
+ data/sample.jsonl 20 invented rows: id, text, label
225
+ data/train.jsonl 48 invented sentences for the fine-tune
226
+ policies/require-review-on-publish.yaml a hello-prod storage policy
227
+ profiles/hello.toml the example's deployment profile
228
+ profiles/hello-infisical.toml the same, with secrets in Infisical
229
+ hooks.py run-hook listener: receipts.jsonl
230
+ tools/calc_server.py stdio MCP server: add, word_count
231
+ pyproject.toml builds the data wheel cairn-pack-hello
232
+ ```
233
+
234
+ Tests: `tests/examples/test_hello_cairn.py`, `tests/core/test_example_pipeline_governance.py`,
235
+ `tests/cli/test_validate_example_pack.py`. The feature proof (ADR-0127 D1) runs all four pipelines
236
+ on real servers in every storage × runtime × bus cell, plus the crash cells: `tests/e2e/hello`,
237
+ driven by `scripts/oss/feature_proof.sh`, which writes `FEATURE_PROOF.md`. Its model is a keyless
238
+ OpenAI-compatible stub, `tests/e2e/model_stub.py`.
@@ -0,0 +1,227 @@
1
+ # hello-cairn: the example pack
2
+
3
+ `hello-cairn` is a domain pack: data plus a little glue, no operators. It runs on the cairn
4
+ engine and its builtin operator packs alone. Its four pipelines each do one thing a new user
5
+ recognises, and together they exercise what the engine adds around a piece of work: gates
6
+ answered by data or by a person, budgets, triggers, an agentic template, an MCP tool,
7
+ retrieval, a run hook, SSH, secrets, and an audit record.
8
+
9
+ | Pipeline | Kind | What it does |
10
+ |---|---|---|
11
+ | `profile-and-publish` | data | Profile a dataset, stop unless the profile passes, redact the summary, run a flow, stage it, ask a person, tag it, publish it, audit it. |
12
+ | `ask-the-docs` | agentic | Retrieve from the pack's docs, ask a model, measure the answer with a tool on the pack's own MCP server, remember it, check its length, ask a person. |
13
+ | `nightly-ops` | operations | Check a secret, run a report on an SSH box, carry it through a flow, propose the follow-up to a person, audit it. Nightly once enabled; a run hook writes receipts. |
14
+ | `tiny-finetune` | ml | Profile training rows, LoRA fine-tune a small model on CPU, gate on its held-out loss, tag it, ask a person, write its card. |
15
+
16
+ ## Quickstart
17
+
18
+ Run everything from the repository root: relative paths in the pipelines' defaults resolve
19
+ against the engine's working directory.
20
+
21
+ ### 1. Install, and point the engine at the example
22
+
23
+ ```bash
24
+ uv sync --extra dev --extra hello --extra rag # add --extra auth if your engine needs it
25
+ export CAIRN_PROFILE=examples/hello-cairn/profiles/hello.toml
26
+ ```
27
+
28
+ `--extra rag` is what lets the engine index the docs corpus at boot (LlamaIndex ingest);
29
+ without it ask-the-docs still runs and retrieves nothing. `auth` is optional: `cairn dev`
30
+ signs you in as dev/dev without it.
31
+
32
+ `cairndev[hello]` installs this pack as the wheel `cairn-pack-hello` plus what its pipelines
33
+ reach beyond the base install: the sqlite-vec store (retrieval and memory), asyncssh
34
+ (`remote/exec`) and DBOS (`CAIRN_RUNTIME=dbos`). Without installing it,
35
+ `export CAIRN_PACK_PATH=examples/hello-cairn` loads the same directory as a dev pack; a dev
36
+ pack declares no entry points, so the run-hook listener does not fire.
37
+
38
+ `profiles/hello.toml` is the deployment profile the example runs under. It carries
39
+ `cairn/profiles/local-dev.toml`'s bindings for the builtin operators (memory, storage, rag,
40
+ mcp), but not local-dev's `eval.delta` binding (a platform operator) or its `llm_gateway` and
41
+ `embedder` backend entries: the model and the embedder come from the engine's environment
42
+ (step 3). It adds `secrets.store` → `secrets/env` (nightly-ops' secret check is a capability
43
+ node and compiles only with a binding, which local-dev does not carry) and the `hello-calc`
44
+ MCP server the pack declares. It pins no runtime, so `CAIRN_RUNTIME=dbos` selects DBOS.
45
+
46
+ ### 2. See what will happen before anything runs
47
+
48
+ ```bash
49
+ cairn pipeline explain hello-cairn/profile-and-publish -e sandbox
50
+ cairn validate examples/hello-cairn # with CAIRN_PROFILE set as in step 1
51
+ ```
52
+
53
+ `cairn validate` needs the example's profile: under local-dev the nightly-job template does
54
+ not compile (no `secrets.store` binding).
55
+
56
+ `explain` lists every leg with the operator class or flow file it resolves to, the gate that
57
+ answers it, and the budget, series and approvers.
58
+
59
+ ### 3. Start an engine
60
+
61
+ ```bash
62
+ export OPENAI_API_KEY=sk-... # ask-the-docs' model call and embeddings
63
+ export RAG_EMBED_PROVIDER=openai # retrieval and memory embed with OpenAI
64
+ export CAIRN_RAG_AUTOINDEX=1 # index rag/docs at boot (needs the `rag` extra)
65
+ export CAIRN_MCP_SERVERS='{"hello-calc": {"transport": "stdio", "command": "python",
66
+ "args": ["examples/hello-cairn/tools/calc_server.py"]}}'
67
+ cairn dev --engine-only --port 8471 & # sqlite engine, in-process runtime, login dev/dev
68
+ cairn remote login -u dev -p dev -e http://localhost:8471
69
+ ```
70
+
71
+ Any OpenAI-compatible endpoint works in place of a key: `OPENAI_BASE_URL`, or
72
+ `LLM_GATEWAY=proxy` with `LLM_GATEWAY_URL`. The engine has no built-in mock model, and
73
+ `CAIRN_BACKEND=mock` is the telemetry backend, not a model. `profile-and-publish` and
74
+ `nightly-ops` call no model.
75
+
76
+ ### 4. Run each pipeline
77
+
78
+ **profile-and-publish**
79
+
80
+ ```bash
81
+ cairn remote launch hello-cairn/profile-and-publish --environment sandbox --watch
82
+ cairn remote advance <pipeline_id> review # the person gate
83
+ cairn remote advance <pipeline_id> promote # the operator's own gate
84
+ ```
85
+
86
+ The pipeline stops a second time at `promote`: `artifact/promote` declares
87
+ `requires_approval`, and a pipeline can add a gate but never remove one an operator declares.
88
+ `cairn remote advance` approves either kind and records the signed-in user as the approver.
89
+
90
+ The record ends `done`: `data-ok` has verdict `data`, `review` `approved`, `promote` the
91
+ tag `hello-sample@published` on the flow leg's registered artifact, and `audit` a
92
+ hash-chained row in `CAIRN_AUDIT_LOG` (default `./audit.jsonl`). `storage/put` writes under
93
+ `CAIRN_ARTIFACTS_DIR` (default `~/.cairn/artifacts`). With `-i min_rows=1000` the data gate
94
+ stops the pipeline (`gate_blocked`) before anything is written. Launch with
95
+ `--environment hello-prod` and the pack's policy
96
+ (`policies/require-review-on-publish.yaml`) stops `stage` for an approval before it writes.
97
+
98
+ **ask-the-docs**
99
+
100
+ ```bash
101
+ cairn remote launch hello-cairn/ask-the-docs --environment sandbox --watch
102
+ cairn remote advance <pipeline_id> review
103
+ cairn remote findings <pipeline_id> # hits, answer, answer_stats, remembered
104
+ ```
105
+
106
+ The `assistant` leg (`flows/assistant.md`) searches the `docs` corpus (`rag/docs`, declared
107
+ in `pack.toml` as `[requires.rag.docs]`), re-ranks, asks the model, calls `word_count` on
108
+ `tools/calc_server.py` through `mcp/call`, and writes the exchange to the `hello_answers`
109
+ memory store. `length` flags answers longer than `max_words`. An un-indexed corpus returns no
110
+ hits and the model says the docs do not cover the question. Retrieval uses sqlite-vec unless
111
+ `RAG_VECTOR_STORE=pgvector`.
112
+
113
+ **nightly-ops**
114
+
115
+ ```bash
116
+ export HELLO_REPORT_TOKEN=... # the secret the job checks for
117
+ export HELLO_SSH_KEY="$(cat ~/.ssh/id_ed25519)" # in the ENGINE's environment
118
+ CAIRN_STORAGE=sqlite:///$HOME/.cairn/dev.db \
119
+ cairn connect add ssh --name hello-ssh --set host=<box> --set user=<user> \
120
+ --set known_hosts=$HOME/.ssh/known_hosts --secret PRIVATE_KEY=HELLO_SSH_KEY
121
+ cairn remote launch hello-cairn/nightly-ops --environment sandbox --watch
122
+ ```
123
+
124
+ The connection goes into the engine's store, hence `CAIRN_STORAGE`; its secret is an env var
125
+ NAME the engine reads. The box needs `sh`, an SFTP subsystem, and `setsid` (Linux) or
126
+ `perl` (macOS): `remote/exec` detaches the job into its own session through whichever is
127
+ present (#2606). `job` (`flows/nightly-job.md`) checks the secret through the profile's
128
+ `secrets.store` binding, then runs `command` on the box and reads back `metrics.json`.
129
+ `propose` waits for a person (`monitor/propose` declares `requires_approval`;
130
+ `cairn remote advance <pipeline_id> propose`). With the wheel installed, every settled leg
131
+ appends a line to `$CAIRN_HOME/hello-cairn/receipts.jsonl` (default `~/.cairn`).
132
+
133
+ For secrets in Infisical, run the engine with
134
+ `CAIRN_PROFILE=examples/hello-cairn/profiles/hello-infisical.toml`, `uv sync --extra
135
+ infisical`, and `INFISICAL_CLIENT_ID`, `INFISICAL_CLIENT_SECRET`, `INFISICAL_PROJECT_ID`
136
+ (and `INFISICAL_HOST` for a self-hosted server). Nothing in the pipeline changes.
137
+
138
+ **tiny-finetune**
139
+
140
+ Needs the builtin train-local pack (inside `cairndev`) and its training stack on the engine's
141
+ box: `cairndev[train]` (torch, transformers, peft, datasets; CPU is enough;
142
+ there is no torch wheel for Intel macOS).
143
+
144
+ ```bash
145
+ cairn remote launch hello-cairn/tiny-finetune --environment sandbox --watch
146
+ cairn remote launch hello-cairn/tiny-finetune -i base_model=Qwen/Qwen2.5-0.5B --watch
147
+ ```
148
+
149
+ `base_model` defaults to the stand-in `sshleifer/tiny-gpt2` (seconds); Qwen2.5-0.5B trains
150
+ in minutes. `loss-ok` stops the pipeline when the held-out loss is above `target_loss`;
151
+ `promote` and `review` each wait for a person.
152
+
153
+ Until the train-local pack was merged (hello-cairn WP3), tiny-finetune was pending in two
154
+ strict xfails and one strict list entry. WP3 removed all three in the change that added the
155
+ pack: `test_tiny_finetune_is_runnable_on_the_public_core`
156
+ (`tests/core/test_example_pipeline_governance.py`) and `test_the_example_pack_validates_clean`
157
+ (`tests/cli/test_validate_example_pack.py`, under `profiles/hello.toml`) are ordinary tests,
158
+ and `tests/core/test_validate_author_gate.py` admits tiny-finetune like every other shipped
159
+ pipeline.
160
+
161
+ ### Triggers
162
+
163
+ The engine arms every declared trigger when it starts: it installs each discovered pipeline
164
+ into the `default` workspace, and each trigger becomes a row that starts its own pipeline
165
+ (ADR-0131, #2631). A trigger fires only when its declaration says `enabled: true`; to turn one
166
+ on, set that in the pipeline file and restart the engine. Out of the box only nightly-ops' cron
167
+ is on. profile-and-publish's dataset `watch` (`on_change: start`), weekly `schedule` and custom
168
+ `webhook` (`POST /triggers/hello-sample`) are `enabled: false`. Turned on, the watch makes each
169
+ tick check whether `hello-sample` gained a registered version, and starts the pipeline only
170
+ when it did. A version is registered by the `engine-check` leg, which records the checked file's content as a version of `hello-sample` (#2632): a changed sample is a new version, and the same sample adds none. With the watch on, a run that registers a new version is followed by one watch-started run on that same content; moving the registration to the job that lands the data is tracked separately. nightly-ops' `table` watch is declared but not armed: a `table` (or `s3`) watch
171
+ needs cairn-platform's governed import, so core names it unarmed. Every armed row is pinned to
172
+ start a `hello-cairn` pipeline.
173
+
174
+ ### Events
175
+
176
+ `profile-and-publish`'s `publish` leg declares `emits: dataset.ready`: when it completes,
177
+ the bus carries that topic with the leg's output as the payload. A leg declared
178
+ `trigger: on:dataset.ready` (in any pipeline) waits for it; `trigger:` also takes `manual`
179
+ and `schedule:<cron>` (#2605). On the Postgres bus the event is durable and replayed to a
180
+ leg that starts waiting later; on the in-process bus only a leg already waiting sees it.
181
+
182
+ A whole pipeline can start on a topic too: `ask-the-docs` declares a `kind: event` trigger on
183
+ `dataset.ready` (`enabled: false`). Turned on, each published `dataset.ready` starts one
184
+ ask-the-docs run (ADR-0131 D4). The listener runs on the engine's leader, so a message
185
+ published while no leader is subscribed starts nothing, on either bus. A pipeline may not
186
+ start on a topic its own legs emit.
187
+
188
+ ### How it runs inside the engine
189
+
190
+ [docs/golden-paths/hello.md](../../docs/golden-paths/hello.md) traces each pipeline from the
191
+ start door to its legs, names where governance fires, and lists the proof for each feature.
192
+
193
+ ### Seeded demo data
194
+
195
+ `cairn dev --seed` and `cairn demo` seed three records of profile-and-publish
196
+ (`cairn/core/demo_seed.py`). The seeder takes the steps from the loaded pipeline's plan, so
197
+ each record carries every step in plan order (`redact`, `promote` and `audit` included):
198
+ the published record has all of them done, the blocked one stops at `data-ok`, and the
199
+ waiting one stops at `review` with the later steps pending (#2608).
200
+
201
+ ## Files
202
+
203
+ ```
204
+ pack.toml manifest (kind = domain): the docs corpus, the MCP server
205
+ __init__.py the one-line shim the entry point targets
206
+ pipelines/*.yaml the four pipelines (the runnable surface)
207
+ flows/engine-check.yaml data flow: one noop stage
208
+ flows/summarise.yaml data flow: noop + noop.slow
209
+ flows/lora.yaml ml flow: train.lora
210
+ flows/assistant.md agentic template of ask-the-docs
211
+ flows/nightly-job.md agentic template of nightly-ops (no model)
212
+ rag/docs/*.txt the docs corpus
213
+ data/sample.jsonl 20 invented rows: id, text, label
214
+ data/train.jsonl 48 invented sentences for the fine-tune
215
+ policies/require-review-on-publish.yaml a hello-prod storage policy
216
+ profiles/hello.toml the example's deployment profile
217
+ profiles/hello-infisical.toml the same, with secrets in Infisical
218
+ hooks.py run-hook listener: receipts.jsonl
219
+ tools/calc_server.py stdio MCP server: add, word_count
220
+ pyproject.toml builds the data wheel cairn-pack-hello
221
+ ```
222
+
223
+ Tests: `tests/examples/test_hello_cairn.py`, `tests/core/test_example_pipeline_governance.py`,
224
+ `tests/cli/test_validate_example_pack.py`. The feature proof (ADR-0127 D1) runs all four pipelines
225
+ on real servers in every storage × runtime × bus cell, plus the crash cells: `tests/e2e/hello`,
226
+ driven by `scripts/oss/feature_proof.sh`, which writes `FEATURE_PROOF.md`. Its model is a keyless
227
+ OpenAI-compatible stub, `tests/e2e/model_stub.py`.
@@ -0,0 +1,11 @@
1
+ """hello-cairn — the example domain pack (#2423, ADR-0127 D2). Data plus glue; this
2
+ shim builds the Pack from the ``pack.toml`` beside it. The glue (``hooks.py``,
3
+ ``tools/calc_server.py``) is reached through an entry point and a profile, never
4
+ imported here."""
5
+ from __future__ import annotations
6
+
7
+ from cairn.core.pack import Pack
8
+
9
+ pack = Pack.from_manifest(__file__)
10
+
11
+ __all__ = ["pack"]
@@ -0,0 +1,20 @@
1
+ {"id": 1, "text": "The soup arrived hot and the bread was fresh.", "label": "positive"}
2
+ {"id": 2, "text": "Delivery took two hours longer than promised.", "label": "negative"}
3
+ {"id": 3, "text": "The packaging was fine, nothing special.", "label": "neutral"}
4
+ {"id": 4, "text": "Best noodles I have had in months.", "label": "positive"}
5
+ {"id": 5, "text": "The order was missing the side salad.", "label": "negative"}
6
+ {"id": 6, "text": "Portion size was about what I expected.", "label": "neutral"}
7
+ {"id": 7, "text": "The courier was polite and quick.", "label": "positive"}
8
+ {"id": 8, "text": "My drink had spilled inside the bag.", "label": "negative"}
9
+ {"id": 9, "text": "I ordered the same dish as last week.", "label": "neutral"}
10
+ {"id": 10, "text": "The dessert was a lovely surprise.", "label": "positive"}
11
+ {"id": 11, "text": "The curry was far too salty to finish.", "label": "negative"}
12
+ {"id": 12, "text": "Paid with the app, it worked as usual.", "label": "neutral"}
13
+ {"id": 13, "text": "Everything was still warm when it came.", "label": "positive"}
14
+ {"id": 14, "text": "The tracking map never updated.", "label": "negative"}
15
+ {"id": 15, "text": "The menu has not changed since spring.", "label": "neutral"}
16
+ {"id": 16, "text": "Great value for a weeknight dinner.", "label": "positive"}
17
+ {"id": 17, "text": "The rice was undercooked again.", "label": "negative"}
18
+ {"id": 18, "text": "Arrived at the time the app estimated.", "label": "neutral"}
19
+ {"id": 19, "text": "The new vegetarian option is excellent.", "label": "positive"}
20
+ {"id": 20, "text": "Support never answered my refund request.", "label": "negative"}
@@ -0,0 +1,48 @@
1
+ {"text": "The tea works well on cold mornings."}
2
+ {"text": "A paper map needed a second try."}
3
+ {"text": "The bicycle bell fits in a coat pocket."}
4
+ {"text": "This notebook looks better in person."}
5
+ {"text": "The garden hose is lighter than it looks."}
6
+ {"text": "A wool scarf smells faintly of cedar."}
7
+ {"text": "The desk lamp made less noise than expected."}
8
+ {"text": "That umbrella arrived a day early."}
9
+ {"text": "The bread knife lasted the whole season."}
10
+ {"text": "A clay mug was easy to set up."}
11
+ {"text": "The tea works well on cold mornings."}
12
+ {"text": "A paper map needed a second try."}
13
+ {"text": "The bicycle bell fits in a coat pocket."}
14
+ {"text": "This notebook looks better in person."}
15
+ {"text": "The garden hose is lighter than it looks."}
16
+ {"text": "A wool scarf smells faintly of cedar."}
17
+ {"text": "The desk lamp made less noise than expected."}
18
+ {"text": "That umbrella arrived a day early."}
19
+ {"text": "The bread knife lasted the whole season."}
20
+ {"text": "A clay mug was easy to set up."}
21
+ {"text": "The tea works well on cold mornings."}
22
+ {"text": "A paper map needed a second try."}
23
+ {"text": "The bicycle bell fits in a coat pocket."}
24
+ {"text": "This notebook looks better in person."}
25
+ {"text": "The garden hose is lighter than it looks."}
26
+ {"text": "A wool scarf smells faintly of cedar."}
27
+ {"text": "The desk lamp made less noise than expected."}
28
+ {"text": "That umbrella arrived a day early."}
29
+ {"text": "The bread knife lasted the whole season."}
30
+ {"text": "A clay mug was easy to set up."}
31
+ {"text": "The tea works well on cold mornings."}
32
+ {"text": "A paper map needed a second try."}
33
+ {"text": "The bicycle bell fits in a coat pocket."}
34
+ {"text": "This notebook looks better in person."}
35
+ {"text": "The garden hose is lighter than it looks."}
36
+ {"text": "A wool scarf smells faintly of cedar."}
37
+ {"text": "The desk lamp made less noise than expected."}
38
+ {"text": "That umbrella arrived a day early."}
39
+ {"text": "The bread knife lasted the whole season."}
40
+ {"text": "A clay mug was easy to set up."}
41
+ {"text": "The tea works well on cold mornings."}
42
+ {"text": "A paper map needed a second try."}
43
+ {"text": "The bicycle bell fits in a coat pocket."}
44
+ {"text": "This notebook looks better in person."}
45
+ {"text": "The garden hose is lighter than it looks."}
46
+ {"text": "A wool scarf smells faintly of cedar."}
47
+ {"text": "The desk lamp made less noise than expected."}
48
+ {"text": "That umbrella arrived a day early."}
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: assistant
3
+ version: 0.1.0
4
+ module: ask
5
+ tags:
6
+ - flow:state_machine
7
+ - domain:example
8
+ description: |
9
+ Answer a question from the pack's own docs: retrieve from the `docs` corpus,
10
+ re-rank, ask a model, measure the answer with a tool on the example's MCP server,
11
+ and remember the exchange. The agentic leg of `ask-the-docs`.
12
+
13
+ flow:
14
+ type: state_machine
15
+ source: behavior_tree
16
+ root:
17
+ type: sequence
18
+ children:
19
+ # Retrieval: the builtin rag/search over the corpus pack.toml declares as
20
+ # [requires.rag.docs] (sqlite-vec by default; pgvector when RAG_VECTOR_STORE
21
+ # says so). Read-only.
22
+ - type: operation
23
+ operator: rag/search
24
+ id: search
25
+ input:
26
+ corpus: docs
27
+ query: $input.question
28
+ top_k: 5
29
+ output:
30
+ hits: $state.findings.hits
31
+ ok: $state.findings.search_ok
32
+ error: $state.findings.search_error
33
+ # Re-rank the hits (identity unless a cross-encoder or Cohere is installed).
34
+ - type: operation
35
+ operator: rag/rerank
36
+ id: rerank
37
+ input:
38
+ query: $input.question
39
+ hits: $default($state.findings.hits, [])
40
+ top_k: 3
41
+ output:
42
+ hits: $state.findings.context
43
+ reranker_used: $state.findings.reranker
44
+ # The model call. The compiler builds it through core's build_chat_model, so
45
+ # the call is redacted (when a redact.text binding is active) and costed.
46
+ - type: action
47
+ ref: answer
48
+ # A tool call on the example's own stdio MCP server (tools/calc_server.py),
49
+ # through the builtin mcp/call operator: governed and audited like any tool call.
50
+ - type: operation
51
+ operator: mcp/call
52
+ id: measure
53
+ input:
54
+ server: hello-calc
55
+ tool: word_count
56
+ args:
57
+ text: $state.findings.answer
58
+ output:
59
+ result: $state.findings.answer_stats
60
+ ok: $state.findings.measure_ok
61
+ error: $state.findings.measure_error
62
+ # Remember the exchange in a local memory store (memory/write, sqlite-vec). A
63
+ # store name is a collection name: letters, digits and underscores only.
64
+ - type: operation
65
+ operator: memory/write
66
+ id: remember
67
+ input:
68
+ store: hello_answers
69
+ record:
70
+ question: $input.question
71
+ answer: $state.findings.answer
72
+ embed_text: $input.question
73
+ output:
74
+ ok: $state.findings.remembered
75
+ id: $state.findings.memory_id
76
+ error: $state.findings.memory_error
77
+
78
+ nodes:
79
+ answer:
80
+ type: prompt
81
+ model: gpt-4o-mini
82
+ capabilities: []
83
+ prompt_section: answer
84
+
85
+ tools:
86
+ required: []
87
+ ---
88
+
89
+ # Ask the docs
90
+
91
+ ## answer
92
+
93
+ You answer a question about the hello-cairn example pack. The retrieved passages
94
+ are in `state.findings.context` (each hit's `payload.content` is the text, and
95
+ `payload.source` names the file). Answer in at most three sentences, using only
96
+ what the passages say, and name the file you used. When the passages do not cover
97
+ the question, say that the docs do not cover it. Do not invent features.