How a solo archival system gets built in public
This plan is now a reference/backlog document, not the primary active execution guide. The trimmed plan became the real execution guide, and its core hardening tranche has been completed.
The following items were completed during the trimmed-plan execution pass:
run_id generationsuccess / degraded / failed)These items remain optional backlog work rather than immediate requirements:
csj/run_model.pycsj/failures.pyuvx pytest ... in this environment rather than plain pytest, because pytest is not installed directly.For Hermes: Use subagent-driven-development skill to implement this plan task-by-task.
Goal: Add operational maturity to the CSJ Collector through observability, explicit run semantics, parser regression protection, typed runtime boundaries, and safer persistence.
Architecture: Keep the existing modular structure. Do not do another broad refactor. Add a thin observability/runtime layer around the current collector flow, then harden parser tests and persistence seams incrementally.
Tech Stack: Python, pytest, dataclasses, JSON/JSONL files, existing CSJ package modules.
scripts/collector.py as stable wrapperscripts/collector_impl.py as compatibility bridgeObjective: Introduce stable filesystem locations for per-run artifacts.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/config.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_state_and_paths.pyStep 1: Write failing test
Add assertions that config/state bootstrap exposes and creates:
RUNS_DIRRUN_EVENTS_FILE (or equivalent if you choose file constant here)Example test shape:
from csj import config
def test_config_exposes_run_artifact_paths():
assert config.RUNS_DIR.name == "csj_runs"
assert config.RUN_EVENTS_FILE.name == "csj_run_events.jsonl"
Step 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_state_and_paths.py -k run_artifact
Expected: FAIL — missing config constants.
Step 3: Implement minimal code
In csj/config.py, add:
RUNS_DIR = DATA_DIR / "csj_runs"RUN_EVENTS_FILE = DATA_DIR / "csj_run_events.jsonl"Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_state_and_paths.py -k run_artifact
Expected: PASS
Step 5: Commit
git add csj/config.py tests/test_state_and_paths.py
git commit -m "feat: add config paths for run artifacts"
Objective: Make run artifact storage part of normal collector bootstrap.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/state.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_state_and_paths.pyStep 1: Write failing test
Add a test that patches paths to a temp dir and verifies ensure_dirs() creates:
RUNS_DIRStep 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_state_and_paths.py -k ensure_dirs
Expected: FAIL — run dir not created.
Step 3: Implement minimal code
Update ensure_dirs() in csj/state.py to create RUNS_DIR.
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_state_and_paths.py -k ensure_dirs
Expected: PASS
Step 5: Commit
git add csj/state.py tests/test_state_and_paths.py
git commit -m "feat: create run artifact directories during bootstrap"
Objective: Create explicit data structures for run state and summaries.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/run_model.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_model.pyStep 1: Write failing test
Add tests for:
Example:
from csj.run_model import RunSummary
def test_run_summary_to_dict_contains_required_fields():
summary = RunSummary(
run_id="2026-04-20T12-00-00Z_test",
mode="native",
started_at="2026-04-20T12:00:00",
completed_at=None,
status="success",
counts={},
timings={},
warnings=[],
errors=[],
anomaly_level="none",
)
data = summary.to_dict()
assert data["run_id"] == "2026-04-20T12-00-00Z_test"
assert data["status"] == "success"
Step 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_model.py
Expected: FAIL — module missing.
Step 3: Implement minimal code
Create csj/run_model.py with dataclasses:
RunSummaryRunStatus constants or enum-lite stringsKeep it minimal and serialization-friendly.
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_model.py
Expected: PASS
Step 5: Commit
git add csj/run_model.py tests/test_run_model.py
git commit -m "feat: add run summary model"
Objective: Make every run uniquely identifiable.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.pyStep 1: Write failing test
Add a test that exercises scrape() with monkeypatched subfunctions and asserts a generated run summary context includes run_id.
If direct testing is awkward, test a helper like:
build_run_id()Step 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k run_id
Expected: FAIL
Step 3: Implement minimal code
In csj/run.py:
build_run_id()scrape()Suggested format:
2026-04-20T09-15-32Z_a1f93b
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k run_id
Expected: PASS
Step 5: Commit
git add csj/run.py tests/test_run_module.py
git commit -m "feat: generate run id for collector runs"
Objective: Persist a durable summary per run while preserving csj_latest.json.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.pyStep 1: Write failing test
Add test for write_run_summary() asserting:
RUNS_DIR/{run_id}.jsonStep 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k write_run_summary
Expected: FAIL — no run-specific file.
Step 3: Implement minimal code
Update write_run_summary() to:
run_idLATEST_FILERUNS_DIR / f"{run_id}.json"Keep current summary schema intact; add new fields rather than replacing old ones.
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k write_run_summary
Expected: PASS
Step 5: Commit
git add csj/run.py tests/test_run_module.py
git commit -m "feat: persist per-run summary artifacts"
Objective: Introduce machine-readable operational events without removing human console output.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/telemetry.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_telemetry.pyStep 1: Write failing test
Test that emit_event() writes one JSON line with fields:
eventrun_idtimestampphaseStep 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_telemetry.py
Expected: FAIL — module missing.
Step 3: Implement minimal code
Create helper(s):
emit_event(path, event, run_id, **fields)utc_now_iso()Keep it tiny.
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_telemetry.py
Expected: PASS
Step 5: Commit
git add csj/telemetry.py tests/test_telemetry.py
git commit -m "feat: add structured telemetry event writer"
Objective: Make run lifecycle visible to machines and operators.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.pyStep 1: Write failing test
Add a monkeypatched test that checks:
run_started emitted when scrape() beginsrun_completed emitted on successful completionStep 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k telemetry
Expected: FAIL
Step 3: Implement minimal code
In scrape():
run_started after bootstraprun_completed before returnrun_id, mode, status, counts if availableStep 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k telemetry
Expected: PASS
Step 5: Commit
git add csj/run.py tests/test_run_module.py
git commit -m "feat: emit lifecycle telemetry for collector runs"
Objective: Track where run time goes.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.py/root/.hermes/skills/research/civil-service-jobs-collector/csj/native.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.pyStep 1: Write failing test
Add assertions that run summaries include timing keys, for example:
startup_msdiscovery_msfetch_mslifecycle_mstotal_msStep 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k timings
Expected: FAIL
Step 3: Implement minimal code
Use time.perf_counter() around:
Keep native internals optional for now; do top-level phase timing first.
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k timings
Expected: PASS
Step 5: Commit
git add csj/run.py csj/native.py tests/test_run_module.py
git commit -m "feat: add phase timing to run summaries"
Objective: Classify runs as success, degraded, or failed.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/run_model.py/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_status.pyStep 1: Write failing test
Create tests for:
Step 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_status.py
Expected: FAIL
Step 3: Implement minimal code
Use current failure/anomaly data from summarize_fetch_results():
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_status.py
Expected: PASS
Step 5: Commit
git add csj/run_model.py csj/run.py tests/test_run_status.py
git commit -m "feat: classify collector runs by explicit status"
Objective: Establish fixture-backed parser testing.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/tests/fixtures/listings//root/.hermes/skills/research/civil-service-jobs-collector/tests/test_native_parser_fixtures.pyStep 1: Write failing test
Test _extract_listings() against fixture and assert at least:
Step 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_native_parser_fixtures.py -k listings
Expected: FAIL — missing fixture/test/module path.
Step 3: Implement minimal code
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_native_parser_fixtures.py -k listings
Expected: PASS
Step 5: Commit
git add tests/fixtures/listings tests/test_native_parser_fixtures.py
git commit -m "test: add fixture-backed listing parser regression test"
Objective: Protect detail parsing against HTML drift.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/tests/fixtures/details//root/.hermes/skills/research/civil-service-jobs-collector/tests/test_native_parser_fixtures.pyStep 1: Write failing test
Add fixture tests for:
If parsing logic is buried in fetch_detail(), test the current method with a stubbed session response.
Step 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_native_parser_fixtures.py -k detail
Expected: FAIL
Step 3: Implement minimal code
Add fixtures and assertions for:
referencegradeclosesattachmentsembedsfull_text presenceStep 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_native_parser_fixtures.py -k detail
Expected: PASS
Step 5: Commit
git add tests/fixtures/details tests/test_native_parser_fixtures.py
git commit -m "test: add detail page parser fixture coverage"
Objective: Catch silent output drift after parsing/normalization changes.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_record_golden_outputs.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/fixtures/golden/Step 1: Write failing test
Take representative parsed raw input and assert stable normalized output from:
normalize_job()finalize_job()Step 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_record_golden_outputs.py
Expected: FAIL
Step 3: Implement minimal code
Add one or two canonical expected JSON outputs covering:
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_record_golden_outputs.py
Expected: PASS
Step 5: Commit
git add tests/test_record_golden_outputs.py tests/fixtures/golden
git commit -m "test: add golden normalized record regression coverage"
Objective: Replace the large dict returned by build_run_context() with a typed object.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/runtime.py/root/.hermes/skills/research/civil-service-jobs-collector/csj/cli.py/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_cli_module.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.pyStep 1: Write failing test
Change/add test expectations so build_run_context() returns an object with attributes, e.g.:
ctx.SCHEMA_VERSIONctx.normalize_jobctx.save_job_recordStep 2: Run test to verify failure
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_cli_module.py /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py
Expected: FAIL
Step 3: Implement minimal code
Create dataclass CollectorRuntime with the current fields from build_run_context().
Update build_run_context() to return CollectorRuntime.
Update csj/run.py to use attributes instead of dict indexing in small, mechanical edits.
Step 4: Run test to verify pass
Run:
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_cli_module.py /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py
Expected: PASS
Step 5: Commit
git add csj/runtime.py csj/cli.py csj/run.py tests/test_cli_module.py tests/test_run_module.py
git commit -m "refactor: replace dict runtime context with typed runtime object"
Objective: Reduce ad hoc dict returns in orchestration.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/run_model.py/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.pyStep 1: Write failing test
Add tests expecting execute_fetches() to return an object with:
jobsfailedStep 2: Run test to verify failure
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k execute_fetches
Expected: FAIL
Step 3: Implement minimal code
Create FetchResult dataclass and update only the narrow return path from execute_fetches().
Step 4: Run test to verify pass
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k execute_fetches
Expected: PASS
Step 5: Commit
git add csj/run_model.py csj/run.py tests/test_run_module.py
git commit -m "refactor: type fetch results in run orchestration"
Objective: Replace vague failure strings with structured categories.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/failures.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_failures.pyStep 1: Write failing test
Test a helper that builds structured failure records like:
detail_fetch_failedattachment_download_failedstate_load_failedStep 2: Run test to verify failure
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_failures.py
Expected: FAIL
Step 3: Implement minimal code
Create a tiny failure model and helper constructors/classifiers.
Step 4: Run test to verify pass
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_failures.py
Expected: PASS
Step 5: Commit
git add csj/failures.py tests/test_failures.py
git commit -m "feat: add structured failure taxonomy"
Objective: Stop silently swallowing enrichment failures in fetch_one_job().
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.pyStep 1: Write failing test
Add tests where:
download_attachments() raisesfetch_youtube_transcripts() raisesAssert:
Step 2: Run test to verify failure
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k asset_failure
Expected: FAIL
Step 3: Implement minimal code
Replace:
except Exception:
pass
with:
Step 4: Run test to verify pass
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py -k asset_failure
Expected: PASS
Step 5: Commit
git add csj/run.py tests/test_run_module.py
git commit -m "feat: surface asset enrichment failures in run telemetry"
Objective: Make corrupted local data visible instead of silently ignored.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.py/root/.hermes/skills/research/civil-service-jobs-collector/csj/records.py/root/.hermes/skills/research/civil-service-jobs-collector/csj/state.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_recovery_paths.pyStep 1: Write failing test
Add tests for:
Assert:
Step 2: Run test to verify failure
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_recovery_paths.py
Expected: FAIL
Step 3: Implement minimal code
At each current swallow point:
Step 4: Run test to verify pass
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_recovery_paths.py
Expected: PASS
Step 5: Commit
git add csj/run.py csj/records.py csj/state.py tests/test_recovery_paths.py
git commit -m "feat: surface malformed local state and record failures"
Objective: Protect critical files from partial writes.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/io_utils.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_atomic_io.pyStep 1: Write failing test
Test that helper writes JSON to a temp file then renames into place.
Step 2: Run test to verify failure
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_atomic_io.py
Expected: FAIL
Step 3: Implement minimal code
Create:
atomic_write_json(path, data)Step 4: Run test to verify pass
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_atomic_io.py
Expected: PASS
Step 5: Commit
git add csj/io_utils.py tests/test_atomic_io.py
git commit -m "feat: add atomic json write helper"
Objective: Harden the most important top-level artifacts first.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/state.py/root/.hermes/skills/research/civil-service-jobs-collector/csj/run.pytests/test_atomic_io.pyStep 1: Write failing test
Add tests asserting the helper is used indirectly by:
save_state()write_run_summary()Step 2: Run test to verify failure
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_state_and_paths.py /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py
Expected: FAIL
Step 3: Implement minimal code
Replace direct write_text() with atomic_write_json() for:
Step 4: Run test to verify pass
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_state_and_paths.py /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_module.py
Expected: PASS
Step 5: Commit
git add csj/state.py csj/run.py tests/test_state_and_paths.py tests/test_run_module.py
git commit -m "feat: use atomic writes for state and run summaries"
Objective: Protect core archival records from truncation/corruption.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/csj/records.py/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_records.pyStep 1: Write failing test
Add test that save_job_record() writes valid JSON through the atomic helper path.
Step 2: Run test to verify failure
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_records.py
Expected: FAIL
Step 3: Implement minimal code
Replace fpath.write_text(...) with atomic_write_json(...).
Step 4: Run test to verify pass
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests/test_records.py
Expected: PASS
Step 5: Commit
git add csj/records.py tests/test_records.py
git commit -m "feat: use atomic writes for job records"
Objective: Clarify what future changes can safely break.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/references/csj-collector-architecture.mdStep 1: Draft section
Add headings:
Step 2: Add concrete statements
Stable:
scripts/collector.pyInternal:
collector_impl.pycsj/Step 3: Review No test needed; review for clarity.
Step 4: Commit
git add references/csj-collector-architecture.md
git commit -m "docs: define stable and internal collector interfaces"
Objective: Prevent future boundary drift.
Files:
/root/.hermes/skills/research/civil-service-jobs-collector/references/csj-collector-architecture.mdStep 1: Add module ownership table
For:
csj/native.pycsj/run.pycsj/records.pycsj/cli.pycsj/state.pyscripts/collector_impl.pyStep 2: Add run architecture section Document:
run_idStep 3: Commit
git add references/csj-collector-architecture.md
git commit -m "docs: add module invariants and run model architecture"
Objective: Verify the new hardening work before full-suite run.
Files: none
Step 1: Run targeted tests
pytest -q \
/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_model.py \
/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_telemetry.py \
/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_run_status.py \
/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_native_parser_fixtures.py \
/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_record_golden_outputs.py \
/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_recovery_paths.py \
/root/.hermes/skills/research/civil-service-jobs-collector/tests/test_atomic_io.py
Expected: PASS
Step 2: Commit if needed
git add -A
git commit -m "test: verify hardening layers with targeted suite"
Objective: Confirm collector health after hardening.
Files: none
Step 1: Run full test suite
pytest -q /root/.hermes/skills/research/civil-service-jobs-collector/tests
Expected: PASS
Step 2: Run CLI smoke checks
python3 /root/.hermes/skills/research/civil-service-jobs-collector/scripts/collector.py --help
python3 /root/.hermes/skills/research/civil-service-jobs-collector/scripts/collector.py --repair-lifecycle --dry-run
Expected: both pass
Step 3: Commit
git add -A
git commit -m "chore: verify collector after observability and hardening work"
If you want the smartest execution order, do these first:
That sequence gives you:
If you only do the highest-value subset, stop after:
That would still materially improve the collector.