runspool 0.2.0__tar.gz → 0.2.1__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.
- runspool-0.2.1/.github/workflows/publish.yml +40 -0
- {runspool-0.2.0 → runspool-0.2.1}/CHANGELOG.md +18 -1
- {runspool-0.2.0 → runspool-0.2.1}/PKG-INFO +1 -1
- {runspool-0.2.0 → runspool-0.2.1}/RELEASING.md +5 -1
- {runspool-0.2.0 → runspool-0.2.1}/docs/cli.md +2 -1
- {runspool-0.2.0 → runspool-0.2.1}/docs/plugins.md +25 -0
- {runspool-0.2.0 → runspool-0.2.1}/docs/workflows.md +4 -0
- {runspool-0.2.0 → runspool-0.2.1}/pyproject.toml +1 -1
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/__init__.py +6 -1
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/cli.py +17 -4
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/config.py +7 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/doctor.py +9 -1
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/runtime.py +10 -1
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/daemon.py +36 -1
- {runspool-0.2.0 → runspool-0.2.1}/tests/test_cli.py +50 -1
- {runspool-0.2.0 → runspool-0.2.1}/tests/test_config.py +10 -0
- runspool-0.2.1/tests/test_daemon_tick.py +89 -0
- runspool-0.2.0/.github/workflows/publish.yml +0 -28
- {runspool-0.2.0 → runspool-0.2.1}/.env.example +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/.github/workflows/ci.yml +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/.gitignore +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/AGENTS.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/CODE_OF_CONDUCT.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/CONTRIBUTING.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/LICENSE +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/README.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/README.zh-CN.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/THIRD_PARTY_NOTICES.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/config.example.yaml +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/docs/README.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/docs/agent-json-output.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/docs/architecture.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/docs/concepts.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/docs/design/0.2-plugin-architecture.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/docs/design-decisions.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/docs/examples.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/docs/writing-steps.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/README.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/config.yaml +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/data/client_profile.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/data/competitors/acme.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/data/competitors/globex.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/data/customer_notes/call-2026-06-10.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/data/pricing_pages/acme-pricing.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/data/requirements.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/data/sources.yaml +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/steps/intel_steps.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/creator-publishing-pipeline/README.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/creator-publishing-pipeline/config.yaml +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/creator-publishing-pipeline/materials/assets/cover.txt +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/creator-publishing-pipeline/materials/notes.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/creator-publishing-pipeline/materials/source.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/local-file-pipeline/README.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/local-file-pipeline/config.yaml +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/local-file-pipeline/inbox/invoice.txt +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/local-file-pipeline/inbox/meeting-notes.txt +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/examples/local-file-pipeline/inbox/support-ticket.txt +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/sample-output/README.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/sample-output/inspect-1.json +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/sample-output/ready/1/classification.json +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/sample-output/ready/1/metadata.json +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/sample-output/ready/1/normalized.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/sample-output/ready/1/source.txt +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/sample-output/ready/1/summary.json +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/sample-output/ready/1/summary.md +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/sample-output/status.json +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/scripts/check-downstream.sh +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/scripts/smoke_examples.sh +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/app.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/builtin_steps/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/builtin_steps/archive.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/builtin_steps/file_intake.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/builtin_steps/markdown_normalize.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/builtin_steps/text_classify.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/builtin_steps/text_summarize.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/builtin_steps/workspace.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/clock.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/commands.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/approval.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/boot.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/builtin.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/bundles.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/cli.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/config_steps.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/credentials.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/steps.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/store.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/tasks.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/core/workflows.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/display.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/doctor.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/engine/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/engine/coordinator.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/engine/gate.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/engine/registry.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/engine/runner.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/engine/step.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/engine/worker_pool.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/kernel/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/kernel/compose.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/kernel/errors.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/kernel/events.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/kernel/fiber.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/kernel/loader.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/kernel/plugin.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/models.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/persistence/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/persistence/connection.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/persistence/event_log.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/persistence/repository.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/persistence/schema.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/persistence/state_machine.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/persistence/step_run_log.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/registry_builder.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/runtime.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/testing/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/testing/store_contract.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/src/runspool/views.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/conftest.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/core/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/core/test_approval.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/core/test_boot.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/core/test_cli_plugins.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/core/test_credentials.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/core/test_m4_review.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/core/test_store_contract.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/engine/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/engine/test_coordinator.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/engine/test_registry.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/engine/test_runner.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/engine/test_runner_contract.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/kernel/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/kernel/test_compose.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/kernel/test_events.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/kernel/test_fiber.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/kernel/test_loader.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/kernel/test_regressions.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/persistence/__init__.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/persistence/test_connection.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/persistence/test_migrations.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/persistence/test_repository.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/persistence/test_settle_races.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/persistence/test_state_machine.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/persistence/test_step_boundary.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/support.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/test_builtin_steps.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/test_display.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/test_models.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/test_registry_builder.py +0 -0
- {runspool-0.2.0 → runspool-0.2.1}/tests/test_views.py +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v5
|
|
20
|
+
|
|
21
|
+
# skip-existing below must not hide a forgotten version bump: a release tag
|
|
22
|
+
# has to match the version being published.
|
|
23
|
+
- name: Check the tag matches the version
|
|
24
|
+
if: github.event_name == 'release'
|
|
25
|
+
run: |
|
|
26
|
+
version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
27
|
+
test "v$version" = "${{ github.event.release.tag_name }}" || {
|
|
28
|
+
echo "release tag ${{ github.event.release.tag_name }} != pyproject version v$version"; exit 1; }
|
|
29
|
+
|
|
30
|
+
# RunSpool and its official plugin(s). Example plugins are not published.
|
|
31
|
+
- name: Build distributions
|
|
32
|
+
run: |
|
|
33
|
+
uv build --package runspool -o dist
|
|
34
|
+
uv build --package runspool-wechat -o dist
|
|
35
|
+
|
|
36
|
+
# A release may bump only some packages: versions already on PyPI are skipped.
|
|
37
|
+
- name: Publish to PyPI
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
39
|
+
with:
|
|
40
|
+
skip-existing: true
|
|
@@ -6,6 +6,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.1] - 2026-10-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`daemon/tick` event**: the daemon emits it after every scheduling round with a
|
|
13
|
+
`DaemonTick` (`round`, `now`), so plugins can do periodic maintenance (sweep a
|
|
14
|
+
cache, rotate files) without a process of their own. A listener that raises is
|
|
15
|
+
logged and skipped; scheduling carries on.
|
|
16
|
+
- **`default_workflow` setting**: the workflow `runspool add` uses without
|
|
17
|
+
`--workflow` (still `local_file` when unset). `runspool doctor` reports a
|
|
18
|
+
default that no workflow defines.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- `runspool.__version__` said 0.1.0; it now reads the installed version.
|
|
22
|
+
- `runspool add` with an unknown workflow printed the error prefix twice
|
|
23
|
+
(`undefined workflow: undefined workflow: 'x'`).
|
|
24
|
+
|
|
9
25
|
## [0.2.0] - 2026-10-04
|
|
10
26
|
|
|
11
27
|
RunSpool is now built from plugins on a small kernel. Everything the engine does —
|
|
@@ -151,6 +167,7 @@ Initial release.
|
|
|
151
167
|
- Documentation (`docs/`), English + Simplified Chinese READMEs, and CI running
|
|
152
168
|
ruff, pytest (Python 3.11/3.12/3.13), and an example smoke test.
|
|
153
169
|
|
|
154
|
-
[Unreleased]: https://github.com/ethan-sun-dev/runspool/compare/v0.2.
|
|
170
|
+
[Unreleased]: https://github.com/ethan-sun-dev/runspool/compare/v0.2.1...HEAD
|
|
171
|
+
[0.2.1]: https://github.com/ethan-sun-dev/runspool/compare/v0.2.0...v0.2.1
|
|
155
172
|
[0.2.0]: https://github.com/ethan-sun-dev/runspool/compare/v0.1.0...v0.2.0
|
|
156
173
|
[0.1.0]: https://github.com/ethan-sun-dev/runspool/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: runspool
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A local-first CLI workflow engine for reliable personal automation.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ethan-sun-dev/runspool
|
|
6
6
|
Project-URL: Repository, https://github.com/ethan-sun-dev/runspool
|
|
@@ -6,7 +6,11 @@ Example plugins (`plugins/runspool-example-*`) are not published.
|
|
|
6
6
|
## 1. Prepare
|
|
7
7
|
|
|
8
8
|
- Set the version in `pyproject.toml` (RunSpool) and in each published plugin's
|
|
9
|
-
`pyproject.toml` (`plugins/runspool-wechat`).
|
|
9
|
+
`pyproject.toml` (`plugins/runspool-wechat`). A plugin with no changes may keep its
|
|
10
|
+
version: the publish workflow skips files already on PyPI. That also means a
|
|
11
|
+
plugin changed *without* a version bump is silently not published: check
|
|
12
|
+
`git diff vPREVIOUS -- plugins/<name>` before keeping a version. The workflow
|
|
13
|
+
does fail when the release tag does not match RunSpool's own version. Plugins declare the RunSpool range
|
|
10
14
|
they support, e.g. `runspool>=0.2,<0.3`; RunSpool refuses to load a plugin outside
|
|
11
15
|
its range (a profile can exempt an exact version with `allow`).
|
|
12
16
|
- Move the `[Unreleased]` notes in `CHANGELOG.md` under the new version and date,
|
|
@@ -46,7 +46,8 @@ runspool add ./chapter-2 --parent 1 --workflow translate
|
|
|
46
46
|
|
|
47
47
|
- `<input>` — the task input (e.g. a file or directory path). An input that
|
|
48
48
|
names an existing file or directory is stored as an absolute path.
|
|
49
|
-
- `-w/--workflow NAME` — workflow to use (default `
|
|
49
|
+
- `-w/--workflow NAME` — workflow to use (default: the profile's `default_workflow`,
|
|
50
|
+
else `local_file`).
|
|
50
51
|
- `--name NAME` — human-readable label (defaults to a value derived by the first
|
|
51
52
|
step, e.g. the file stem).
|
|
52
53
|
- `--meta KEY=VALUE` — task metadata, repeatable. Values are strings; steps read
|
|
@@ -266,6 +266,7 @@ The events RunSpool itself dispatches:
|
|
|
266
266
|
| --- | --- | --- |
|
|
267
267
|
| `step/pre-execute` | waterfall | `(request, next)`: decides whether a claimed step may run |
|
|
268
268
|
| `approval/request` | emit | `(task)`: a task has started waiting for approval |
|
|
269
|
+
| `daemon/tick` | emit | `(tick)`: the daemon finished a scheduling round (`DaemonTick`: `round`, `now`) |
|
|
269
270
|
| `internal/status` | emit | `(fiber)`: a plugin's lifecycle state changed (kernel internal) |
|
|
270
271
|
|
|
271
272
|
`step/pre-execute` and `approval/request` are dispatched on the worker thread
|
|
@@ -336,6 +337,30 @@ notification plugin listens to it:
|
|
|
336
337
|
ctx.on("approval/request", lambda task: notify_owner(f"task {task['id']} awaits approval"))
|
|
337
338
|
```
|
|
338
339
|
|
|
340
|
+
### `daemon/tick`
|
|
341
|
+
|
|
342
|
+
The resident daemon emits it once per round, after scheduling, with a
|
|
343
|
+
`runspool.daemon.DaemonTick` (`round`: 1, 2, ... for this daemon run; `now`:
|
|
344
|
+
`time.time()`). It is the place for periodic maintenance: sweeping a cache,
|
|
345
|
+
rotating files, reporting. `run` and `run_until_idle` do not emit it.
|
|
346
|
+
|
|
347
|
+
Listeners run on the daemon's thread between rounds, so keep them quick and
|
|
348
|
+
throttle them yourself; hand long work to a thread. One that raises is logged and
|
|
349
|
+
skipped. A daily sweep:
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
last_day = None
|
|
353
|
+
|
|
354
|
+
def sweep(tick):
|
|
355
|
+
global last_day
|
|
356
|
+
day = time.strftime("%Y-%m-%d", time.localtime(tick.now))
|
|
357
|
+
if day != last_day: # once a day, and once after each daemon start
|
|
358
|
+
last_day = day
|
|
359
|
+
remove_older_than(cache_dir, days=7)
|
|
360
|
+
|
|
361
|
+
ctx.on("daemon/tick", sweep)
|
|
362
|
+
```
|
|
363
|
+
|
|
339
364
|
## Side effects and approvals
|
|
340
365
|
|
|
341
366
|
Set `side_effect = True` on a step whose effects leave RunSpool: publishing,
|
|
@@ -42,6 +42,7 @@ concurrency: # per-step quota (default 1)
|
|
|
42
42
|
ingest_file: 4
|
|
43
43
|
|
|
44
44
|
first_task_id: 1000 # number new tasks from here (never lowers it)
|
|
45
|
+
default_workflow: local_file # what `runspool add` uses without --workflow
|
|
45
46
|
|
|
46
47
|
workflows:
|
|
47
48
|
local_file:
|
|
@@ -67,6 +68,9 @@ patch: [] # insert / modify / disable plugin entries
|
|
|
67
68
|
(`ready/<id>/`). A relative path is resolved against the config file's
|
|
68
69
|
directory, so the same profile means the same workspace whichever directory a
|
|
69
70
|
command or the daemon starts in.
|
|
71
|
+
- **default_workflow** — the workflow `runspool add` uses when `--workflow` is not
|
|
72
|
+
given (default `local_file`). It may name a workflow a plugin contributes;
|
|
73
|
+
`runspool doctor` reports it if no such workflow is defined.
|
|
70
74
|
- **scheduler.max_retries** — the retry budget stamped onto each new task. Set it
|
|
71
75
|
to `0` to make the first failure terminal (it becomes `manual_required`
|
|
72
76
|
immediately) — useful when failures mean "bad input", not "transient glitch".
|
|
@@ -9,6 +9,11 @@ readable as JSON for scripts and AI agents.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
from importlib import metadata
|
|
13
|
+
|
|
14
|
+
try: # the installed distribution is the single source of the version
|
|
15
|
+
__version__ = metadata.version("runspool")
|
|
16
|
+
except metadata.PackageNotFoundError: # a bare source tree
|
|
17
|
+
__version__ = "0+unknown"
|
|
13
18
|
|
|
14
19
|
__all__ = ["__version__"]
|
|
@@ -105,7 +105,12 @@ def init(
|
|
|
105
105
|
@app.command()
|
|
106
106
|
def add(
|
|
107
107
|
input: str = typer.Argument(..., help="The task input (e.g. a file path)."),
|
|
108
|
-
workflow: str = typer.Option(
|
|
108
|
+
workflow: str = typer.Option(
|
|
109
|
+
None,
|
|
110
|
+
"--workflow",
|
|
111
|
+
"-w",
|
|
112
|
+
help="Workflow name (default: the profile's default_workflow, else local_file).",
|
|
113
|
+
),
|
|
109
114
|
name: str = typer.Option(None, "--name", help="Human-readable task name."),
|
|
110
115
|
force: bool = typer.Option(False, "--force", help="Allow a duplicate active task."),
|
|
111
116
|
parent: int = typer.Option(None, "--parent", help="Id of the task this one derives from."),
|
|
@@ -121,9 +126,15 @@ def add(
|
|
|
121
126
|
typer.echo(f"--meta expects KEY=VALUE, got {item!r}")
|
|
122
127
|
raise typer.Exit(1)
|
|
123
128
|
metadata[key] = value
|
|
129
|
+
ctx = _ctx()
|
|
130
|
+
if workflow is None: # only when -w is absent: an empty -w "" is still an error
|
|
131
|
+
workflow = ctx.config.add_workflow()
|
|
132
|
+
if workflow not in ctx.config.workflows:
|
|
133
|
+
typer.echo(f"undefined workflow: {workflow}")
|
|
134
|
+
raise typer.Exit(1)
|
|
124
135
|
try:
|
|
125
136
|
task_id = commands.add_task(
|
|
126
|
-
|
|
137
|
+
ctx,
|
|
127
138
|
input,
|
|
128
139
|
workflow=workflow,
|
|
129
140
|
name=name,
|
|
@@ -136,8 +147,10 @@ def add(
|
|
|
136
147
|
raise typer.Exit(1) from exc
|
|
137
148
|
except KeyError as exc:
|
|
138
149
|
missing = exc.args[0] if exc.args else exc
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
if parent is not None and missing == parent:
|
|
151
|
+
typer.echo(f"undefined parent task: {parent}")
|
|
152
|
+
else:
|
|
153
|
+
typer.echo(f"error: missing {missing!r}")
|
|
141
154
|
raise typer.Exit(1) from exc
|
|
142
155
|
typer.echo(f"Created task {task_id}")
|
|
143
156
|
|
|
@@ -73,6 +73,9 @@ class AppConfig(BaseModel):
|
|
|
73
73
|
# Number new tasks from here on (never lowers the numbering). For a database that
|
|
74
74
|
# continues an older system whose task ids are used as keys elsewhere.
|
|
75
75
|
first_task_id: int | None = Field(default=None, ge=1)
|
|
76
|
+
# The workflow `runspool add` uses when no --workflow is given (default: local_file).
|
|
77
|
+
# Plugins may contribute it, so it is checked by doctor rather than at load time.
|
|
78
|
+
default_workflow: str | None = Field(default=None, min_length=1)
|
|
76
79
|
steps: dict[str, StepPluginConfig] = Field(default_factory=dict)
|
|
77
80
|
|
|
78
81
|
# Directory of the config file (set by load()); used to resolve plugin_paths.
|
|
@@ -112,6 +115,10 @@ class AppConfig(BaseModel):
|
|
|
112
115
|
raise KeyError(f"undefined workflow: {name!r}")
|
|
113
116
|
return WorkflowDef(name=name, steps=self.workflows[name].steps)
|
|
114
117
|
|
|
118
|
+
def add_workflow(self) -> str:
|
|
119
|
+
"""The workflow a new task gets when none is named."""
|
|
120
|
+
return self.default_workflow or "local_file"
|
|
121
|
+
|
|
115
122
|
def step_quota(self, step: str) -> int:
|
|
116
123
|
return self.concurrency.get(step, 1)
|
|
117
124
|
|
|
@@ -72,7 +72,15 @@ class DoctorService:
|
|
|
72
72
|
checks.append(Check("database", db_ok, db_detail))
|
|
73
73
|
|
|
74
74
|
n_workflows = len(config.workflows)
|
|
75
|
-
|
|
75
|
+
detail = f"{n_workflows} defined"
|
|
76
|
+
ok = n_workflows > 0
|
|
77
|
+
if config.default_workflow is not None:
|
|
78
|
+
if config.default_workflow in config.workflows:
|
|
79
|
+
detail += f"; default: {config.default_workflow}"
|
|
80
|
+
else:
|
|
81
|
+
ok = False
|
|
82
|
+
detail += f"; default_workflow {config.default_workflow!r} is not defined"
|
|
83
|
+
checks.append(Check("workflows", ok, detail))
|
|
76
84
|
|
|
77
85
|
# Every step referenced by a workflow resolves (built-in or plugin), and every
|
|
78
86
|
# lazily registered step imports. This catches typos and broken plugin imports
|
|
@@ -90,6 +90,7 @@ class RuntimeService:
|
|
|
90
90
|
self.store = store
|
|
91
91
|
self.steps = steps
|
|
92
92
|
self._startup = startup
|
|
93
|
+
self._ctx = ctx
|
|
93
94
|
self.gate = Gate(ctx, steps) if ctx is not None else None
|
|
94
95
|
|
|
95
96
|
def ensure_ready(self) -> None:
|
|
@@ -130,8 +131,16 @@ class RuntimeService:
|
|
|
130
131
|
return Coordinator(store.repo, store.log, registry, runner, pool, self.config)
|
|
131
132
|
|
|
132
133
|
def build_daemon(self) -> Daemon:
|
|
134
|
+
"""The resident daemon. After each round it emits ``daemon/tick`` with a
|
|
135
|
+
:class:`~runspool.daemon.DaemonTick`, so plugins can do periodic maintenance
|
|
136
|
+
(sweep caches, rotate files). Listeners run on the daemon thread: keep them
|
|
137
|
+
quick, throttle them yourself (``tick.now``), and hand long work to a thread.
|
|
138
|
+
A listener that raises is logged and skipped. ``run`` / ``run_until_idle``
|
|
139
|
+
do not emit it: it belongs to the long-running process."""
|
|
133
140
|
self.ensure_ready()
|
|
134
|
-
|
|
141
|
+
ctx = self._ctx
|
|
142
|
+
on_tick = (lambda tick: ctx.emit("daemon/tick", tick)) if ctx is not None else None
|
|
143
|
+
return Daemon(self.build_coordinator(), self.config, on_tick=on_tick)
|
|
135
144
|
|
|
136
145
|
def recover(self) -> None:
|
|
137
146
|
any_workflow = next(iter(self.config.workflows))
|
|
@@ -5,6 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
7
|
import threading
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
8
11
|
from pathlib import Path
|
|
9
12
|
from typing import Any
|
|
10
13
|
|
|
@@ -26,11 +29,32 @@ def read_pid(pid_file: Path) -> int | None:
|
|
|
26
29
|
return None
|
|
27
30
|
|
|
28
31
|
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class DaemonTick:
|
|
34
|
+
"""One daemon round, after its scheduling work: what ``daemon/tick`` carries.
|
|
35
|
+
|
|
36
|
+
``round`` counts from 1 for each daemon run; ``now`` is ``time.time()``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
round: int
|
|
40
|
+
now: float
|
|
41
|
+
|
|
42
|
+
|
|
29
43
|
class Daemon:
|
|
30
|
-
def __init__(
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
coordinator: Coordinator,
|
|
47
|
+
config: Any,
|
|
48
|
+
*,
|
|
49
|
+
on_tick: Callable[[DaemonTick], None] | None = None,
|
|
50
|
+
) -> None:
|
|
31
51
|
self.coordinator = coordinator
|
|
32
52
|
self.config = config
|
|
53
|
+
# Called once per round after scheduling (maintenance hooks). It runs on the
|
|
54
|
+
# daemon's own thread, so it must return quickly.
|
|
55
|
+
self.on_tick = on_tick
|
|
33
56
|
self._stop = threading.Event()
|
|
57
|
+
self._round = 0
|
|
34
58
|
|
|
35
59
|
def recover(self) -> None:
|
|
36
60
|
# Startup recovery: recover_interrupted only changes task_status / lock
|
|
@@ -53,6 +77,16 @@ class Daemon:
|
|
|
53
77
|
self.coordinator.reclaim_stale(timeout)
|
|
54
78
|
self.coordinator.tick()
|
|
55
79
|
|
|
80
|
+
def _tick(self) -> None:
|
|
81
|
+
if self.on_tick is None:
|
|
82
|
+
return
|
|
83
|
+
self._round += 1
|
|
84
|
+
# Maintenance must never stop scheduling: log and carry on.
|
|
85
|
+
try:
|
|
86
|
+
self.on_tick(DaemonTick(round=self._round, now=time.time()))
|
|
87
|
+
except Exception: # noqa: BLE001 - a broken hook must not kill the daemon
|
|
88
|
+
logger.exception("daemon maintenance hook failed; continuing")
|
|
89
|
+
|
|
56
90
|
def request_stop(self) -> None:
|
|
57
91
|
self._stop.set()
|
|
58
92
|
|
|
@@ -70,6 +104,7 @@ class Daemon:
|
|
|
70
104
|
self.run_once()
|
|
71
105
|
except Exception: # noqa: BLE001 - tolerate transient errors
|
|
72
106
|
logger.exception("daemon tick failed; continuing")
|
|
107
|
+
self._tick()
|
|
73
108
|
self._stop.wait(timeout=interval)
|
|
74
109
|
finally:
|
|
75
110
|
self.coordinator.pool.shutdown(wait=True)
|
|
@@ -186,7 +186,7 @@ def test_add_unknown_workflow(tmp_path):
|
|
|
186
186
|
cfg = _init(tmp_path)
|
|
187
187
|
result = _invoke(cfg, "add", "x", "--workflow", "nope")
|
|
188
188
|
assert result.exit_code == 1
|
|
189
|
-
assert "undefined workflow"
|
|
189
|
+
assert result.output.strip() == "undefined workflow: nope"
|
|
190
190
|
|
|
191
191
|
|
|
192
192
|
def test_wake_runs_a_deferred_task_now(tmp_path):
|
|
@@ -262,3 +262,52 @@ def test_bad_engine_settings_are_reported_without_echoing_values(tmp_path):
|
|
|
262
262
|
assert result.exit_code == 1
|
|
263
263
|
assert "max_retries" in result.output and "sekrit" not in result.output
|
|
264
264
|
assert "Traceback" not in result.output
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _profile_with_default(tmp_path, default):
|
|
268
|
+
cfg = tmp_path / "runspool.yaml"
|
|
269
|
+
cfg.write_text(
|
|
270
|
+
f"workspace_root: {tmp_path / 'ws'}\n"
|
|
271
|
+
f"default_workflow: {default}\n"
|
|
272
|
+
"workflows:\n notes:\n steps: [ingest_file, archive]\n",
|
|
273
|
+
encoding="utf-8",
|
|
274
|
+
)
|
|
275
|
+
return cfg
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_add_uses_the_profile_default_workflow(tmp_path):
|
|
279
|
+
cfg = _profile_with_default(tmp_path, "notes")
|
|
280
|
+
src = tmp_path / "a.txt"
|
|
281
|
+
src.write_text("hi", encoding="utf-8")
|
|
282
|
+
assert _invoke(cfg, "add", str(src)).exit_code == 0
|
|
283
|
+
assert json.loads(_invoke(cfg, "status", "1", "--json").output)["workflow"] == "notes"
|
|
284
|
+
doctor = _invoke(cfg, "doctor")
|
|
285
|
+
assert "default: notes" in doctor.output
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_an_undefined_default_workflow_is_reported(tmp_path):
|
|
289
|
+
cfg = _profile_with_default(tmp_path, "nope")
|
|
290
|
+
src = tmp_path / "a.txt"
|
|
291
|
+
src.write_text("hi", encoding="utf-8")
|
|
292
|
+
result = _invoke(cfg, "add", str(src))
|
|
293
|
+
assert result.exit_code == 1 and "undefined workflow: nope" in result.output
|
|
294
|
+
doctor = _invoke(cfg, "doctor")
|
|
295
|
+
assert doctor.exit_code == 1
|
|
296
|
+
assert "default_workflow 'nope' is not defined" in doctor.output
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_without_a_default_add_still_uses_local_file(tmp_path):
|
|
300
|
+
cfg = _init(tmp_path)
|
|
301
|
+
src = tmp_path / "a.txt"
|
|
302
|
+
src.write_text("hi", encoding="utf-8")
|
|
303
|
+
assert _invoke(cfg, "add", str(src)).exit_code == 0
|
|
304
|
+
assert json.loads(_invoke(cfg, "status", "1", "--json").output)["workflow"] == "local_file"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_an_empty_workflow_option_is_an_error_not_the_default(tmp_path):
|
|
308
|
+
cfg = _profile_with_default(tmp_path, "notes")
|
|
309
|
+
src = tmp_path / "a.txt"
|
|
310
|
+
src.write_text("hi", encoding="utf-8")
|
|
311
|
+
result = _invoke(cfg, "add", str(src), "--workflow", "")
|
|
312
|
+
assert result.exit_code == 1
|
|
313
|
+
assert result.output.strip() == "undefined workflow:"
|
|
@@ -48,3 +48,13 @@ def test_plugin_import_alias_and_base_dir(tmp_path):
|
|
|
48
48
|
)
|
|
49
49
|
assert cfg.steps["custom"].import_target == "mod:Cls"
|
|
50
50
|
assert cfg.resolved_plugin_paths() == [tmp_path.resolve() / "steps"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_package_version_matches_the_project():
|
|
54
|
+
import tomllib
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
import runspool
|
|
58
|
+
|
|
59
|
+
project = tomllib.loads((Path(__file__).parents[1] / "pyproject.toml").read_text("utf-8"))
|
|
60
|
+
assert runspool.__version__ == project["project"]["version"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""The daemon's maintenance hook: ``daemon/tick`` once per round, never fatal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
from runspool.app import load_context
|
|
10
|
+
from runspool.daemon import Daemon, DaemonTick
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Coordinator:
|
|
14
|
+
def __init__(self, *, fail: bool = False) -> None:
|
|
15
|
+
self.fail = fail
|
|
16
|
+
self.ticks = 0
|
|
17
|
+
self.pool = SimpleNamespace(shutdown=lambda wait=True: None)
|
|
18
|
+
|
|
19
|
+
def reclaim_stale(self, timeout):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def tick(self):
|
|
23
|
+
self.ticks += 1
|
|
24
|
+
if self.fail:
|
|
25
|
+
raise RuntimeError("scheduling broke")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _config():
|
|
29
|
+
return SimpleNamespace(
|
|
30
|
+
worker_pool=SimpleNamespace(heartbeat_timeout_seconds=60),
|
|
31
|
+
scheduler=SimpleNamespace(poll_interval_seconds=0),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run(daemon: Daemon, *, until) -> None:
|
|
36
|
+
daemon.recover = lambda: None # no store here
|
|
37
|
+
thread = threading.Thread(target=daemon.run, kwargs={"poll_interval_seconds": 0.001})
|
|
38
|
+
thread.start()
|
|
39
|
+
for _ in range(2000):
|
|
40
|
+
if until():
|
|
41
|
+
break
|
|
42
|
+
threading.Event().wait(0.001)
|
|
43
|
+
daemon.request_stop()
|
|
44
|
+
thread.join(timeout=5)
|
|
45
|
+
assert not thread.is_alive()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_each_round_ends_with_a_tick():
|
|
49
|
+
seen: list[DaemonTick] = []
|
|
50
|
+
coordinator = _Coordinator()
|
|
51
|
+
daemon = Daemon(coordinator, _config(), on_tick=seen.append)
|
|
52
|
+
_run(daemon, until=lambda: len(seen) >= 3)
|
|
53
|
+
assert [t.round for t in seen[:3]] == [1, 2, 3]
|
|
54
|
+
assert seen[0].now <= seen[2].now
|
|
55
|
+
assert coordinator.ticks >= 3
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_a_failing_hook_or_round_does_not_stop_the_daemon(caplog):
|
|
59
|
+
calls = []
|
|
60
|
+
|
|
61
|
+
def hook(tick):
|
|
62
|
+
calls.append(tick.round)
|
|
63
|
+
raise ValueError("hook broke")
|
|
64
|
+
|
|
65
|
+
with caplog.at_level(logging.ERROR):
|
|
66
|
+
daemon = Daemon(_Coordinator(fail=True), _config(), on_tick=hook)
|
|
67
|
+
_run(daemon, until=lambda: len(calls) >= 3)
|
|
68
|
+
assert calls[:3] == [1, 2, 3] # the hook still runs after a failed round
|
|
69
|
+
assert "maintenance hook failed" in caplog.text
|
|
70
|
+
assert "daemon tick failed" in caplog.text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_the_runtime_daemon_emits_daemon_tick_to_plugins(tmp_path, caplog):
|
|
74
|
+
profile = tmp_path / "runspool.yaml"
|
|
75
|
+
profile.write_text(f"workspace_root: {tmp_path / 'ws'}\n", encoding="utf-8")
|
|
76
|
+
ctx = load_context(profile)
|
|
77
|
+
seen = []
|
|
78
|
+
|
|
79
|
+
def broken(tick):
|
|
80
|
+
raise RuntimeError("listener broke")
|
|
81
|
+
|
|
82
|
+
ctx.kernel.root.plugin(lambda c, config: c.on("daemon/tick", broken))
|
|
83
|
+
ctx.kernel.root.plugin(lambda c, config: c.on("daemon/tick", seen.append))
|
|
84
|
+
daemon = ctx.service("runtime").build_daemon()
|
|
85
|
+
tick = DaemonTick(round=1, now=0.0)
|
|
86
|
+
with caplog.at_level(logging.ERROR):
|
|
87
|
+
daemon.on_tick(tick)
|
|
88
|
+
assert seen == [tick] # one listener failing does not starve the others
|
|
89
|
+
assert "listener broke" in caplog.text
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
name: Publish
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
release:
|
|
6
|
-
types: [published]
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
publish:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
id-token: write
|
|
14
|
-
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Install uv
|
|
19
|
-
uses: astral-sh/setup-uv@v5
|
|
20
|
-
|
|
21
|
-
# RunSpool and its official plugin(s). Example plugins are not published.
|
|
22
|
-
- name: Build distributions
|
|
23
|
-
run: |
|
|
24
|
-
uv build --package runspool -o dist
|
|
25
|
-
uv build --package runspool-wechat -o dist
|
|
26
|
-
|
|
27
|
-
- name: Publish to PyPI
|
|
28
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/data/customer_notes/call-2026-06-10.md
RENAMED
|
File without changes
|
{runspool-0.2.0 → runspool-0.2.1}/examples/client-intel-brief/data/pricing_pages/acme-pricing.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{runspool-0.2.0 → runspool-0.2.1}/examples/creator-publishing-pipeline/materials/assets/cover.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|