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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@
# quote any value containing spaces, <, >, or other shell metacharacters, and never
# use a variable name with a hyphen. A malformed line kills every tick.
#
# Every YAAS_* knob below is OPTIONAL and shown with its built-in default, so an
# empty .env (plus your Slack app values) is a working install. Uncomment only what
# you want to change.
# Every YAAS_* knob below is OPTIONAL and shown with its built-in default. Uncomment
# only what you want to change.

# ── Slack app (create your own — PKCE OAuth, no client secret) ─────────────
# REQUIRED. These four have no default: an empty .env is NOT a working install.
# The free Python Slack checkers and reaction sweep are enabled by default and require
# these four values plus the Keychain token installed by setup.sh.
#
# If you have no Slack app and intend to access Slack only from scheduled paid workers
# through Claude/Codex/Cursor MCP, disable the local adapter. Existing slack_* entries
# remain stored with unchanged watermarks; the Python loop quietly ignores them.
# Scheduled and non-Slack watches continue normally.
#
# YAAS_SLACK_CHECKERS_ENABLED=0
#
# YAAS talks to Slack via a user-token OAuth app in YOUR workspace, which you
# have to create before these values can exist. Get the ready-to-paste manifest
Expand Down Expand Up @@ -157,11 +164,11 @@ YAAS_CODEX_PERMISSION_MODE=workspace-write
# YAAS_WORKER_STALL_SECONDS=900

# Model overrides. Leave unset to use each CLI's own default.
# Recommended default: Opus 5 at low thinking. Triage work is bounded and well-scoped (the
# Recommended default: Opus 4.8 at medium thinking. Triage work is bounded and well-scoped (the
# quest's context.md carries the decision rules), so low effort is fast and cheap while still
# using the most capable model. Raise the effort per-install if your quests need deeper reasoning.
YAAS_CLAUDE_MODEL=claude-opus-5
YAAS_CLAUDE_EFFORT=low
YAAS_CLAUDE_MODEL=claude-sonnet-5
YAAS_CLAUDE_EFFORT=medium
# YAAS_CODEX_MODEL=
# YAAS_CURSOR_MODEL=

Expand Down
7 changes: 7 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Default owner for every file in this repository.
#
# A GitHub handle only resolves inside its own tenant, so this line cannot name both
# mirrors at once: listing the other tenant's handle makes the whole entry an "Unknown
# owner" error there. The handle below is correct for this repository; the publish
# script rewrites it when it delivers a snapshot to the other tenant.
* @guangmian-circle
4 changes: 2 additions & 2 deletions .github/workflows/scan.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Scan
on:
pull_request:
branches: [main]
branches: [master]
push:
branches: [main]
branches: [master]
jobs:
scan:
if: github.event_name == 'pull_request'
Expand Down
19 changes: 19 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ OAuth uses PKCE, stores the resulting user token in the operating-system Keychai
Python checkers a supported credential they can use directly. The app is the workspace-approved
identity; the token is the local key that lets the no-LLM polling plane act as that identity.

An installation without that app can set `YAAS_SLACK_CHECKERS_ENABLED=0`. This disables the
entire local Slack adapter: `slack_*` entries are not executed, their watermarks do not move, and
the global reaction sweep does not run. Schedule and non-Slack checkers are unaffected. Agent
workers keep their own connector configuration, so a scheduled paid sweep may still use Slack
through Claude, Codex, or Cursor MCP. The switch lives at the adapter seam because all local Slack
watch types share the same credential and failure mode.

The alternative is one published Sidequestor Marketplace app shared by every workspace. That
would reduce setup to an install-and-approve flow, but it would make every installation depend on
a centrally owned app identity, its publication status, and its ongoing support. A hosted OAuth
Expand Down Expand Up @@ -313,6 +320,18 @@ state/quests/active/<quest_id>/
Lifecycle: `active/` → `completed/` or `archived/`. A quest is a folder. No database, no schema
migration.

Workspace-level briefings are separate from quests:

```
state/briefs/
└── *.md free-form Markdown briefing names
```

`state/briefs/` is the canonical briefing store. The dashboard reads and renders these files;
sorts them newest-first by filesystem creation time; and treats cadence words in filenames only as
optional display hints. Slack posts are delivery copies. A briefing does not belong in a quest
folder because it may summarize several quests and other workspace activity.

---

## 10. The file map
Expand Down
84 changes: 84 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Changelog

All notable changes to Sidequestor (YaaS). The current version also lives in `VERSION`.

Versions are dated by the day the snapshot was published.

## Unreleased

### Changed
- Every Markdown file in `state/briefs/` is now displayed. Briefing names are free-form, ordering
comes from filesystem creation time, and cadence words in names are optional display hints.

## 2.5.1 - 2026-08-17

A same-day patch release: eight defects found by auditing the 2.5 snapshot, plus one reported from
using the dashboard. No new features, no config changes required.

### Fixed
- Watermark claims are truncated to Slack's 6 decimals, never rounded. `checkers/result.py`
formatted `advance_to` with `:.6f`, which is round-half-even and could move a claim FORWARD of the
point actually proven covered; a message sitting exactly on the rounded microsecond was then read
as already-seen forever. `slack_dm` and `slack_mention` were also pre-rounding at the call site,
which made the emit-side fix a no-op for the two checkers that reach it.
- `result.emit()` no longer raises on a non-finite claim, as its docstring promises.
- `classify()` no longer erases a numeric `0` watermark claim through falsiness. An erased claim is
not a hold: the commit layer falls back to `now - lag` and jumps the watermark to NOW.
- The dashboard no longer rebuilds DOM it has not changed. Every 2s poll rewrote whole subtrees even
when the payload was identical, which made the prompt box flicker and reset a long draft's scroll
in the review interface. Writes now compare first, and a textarea's value is left alone when it
matches.
- Briefings are no longer rebuilt on every poll and discarded (~114ms and 75KB of JSON per poll on a
150-file archive). They are served on demand from `/api/briefs`.
- Briefings have one canonical timestamp, `at`, derived from the filename with an explicit UTC
offset. Dates were previously read from the filename as a bare local wall clock, or from the file's
mtime, which are different things.
- `build_briefs()` checks the `<date>_<hhmm>_<type>` filename prefix, so a stray `.md` in
`state/briefs/` can no longer sort above every dated file and be served as the newest briefing. A
trailing segment is kept in the type rather than silently trimmed.
- The markdown renderer's link placeholder can no longer be forged from prose, and a `$&` in a URL
or label is inserted literally instead of being expanded as a substitution pattern.

### Changed
- A fractional value for a whole-number knob is now refused at startup instead of being floored to 0
and silently disabling the cap it was meant to set. Knobs whose reader honours a fraction
(`YAAS_STALE_REPLY_HOURS`, `YAAS_MAX_SPEND_*`) still accept one.

### Added
- `yaas-triage/tests/unit/dashboard-render.test.sh`: the dashboard's renderer, date helpers and
write guards are tested by behaviour, running the shipped implementations rather than asserting
that the file contains certain strings. Skips cleanly where `node` is absent, naming what it did
not cover.

## 2.5 - 2026-08-17

The first versioned release. Everything below landed after the initial public import.

### Added
- Dashboard v2: a manual-review surface with its own overlay, worker state, clearer metrics, a
revise-and-resubmit path, and the Field Guide theme.
- A quick start (`QUICKSTART.md`) written for someone who has never run the loop.
- Unit coverage for watermark precision, the re-armed approval watch, and the edit route.
- A `doctor.sh` Python version check, so an unsupported interpreter fails with a readable message
instead of a `TypeError` mid-dispatch.

### Changed
- Watermarks are stored at Slack's 6-decimal precision, so a message can no longer be re-read or
skipped because of a rounded timestamp.
- Timeline events are stamped by the logging helper rather than the worker, which has no clock.
- README rewritten around the agent-driven install and the Slack-first workflow; `ARCHITECTURE.md`
and the shipped skills realigned with the runtime that actually runs.
- The approval watch re-arms on every non-terminal transition, so a reviewed item cannot stall.
- Quests are documented as full local agent missions, including how their watches adapt.
- Python 3.9 is supported again; duplicated helpers consolidated.

### Fixed
- Security hardening: path traversal, the approval gate, the parser, a dispatch loop, and token
exposure.
- Uninitialised quests are distinguished from empty ones, and the create modal no longer
overpromises.
- The dashboard logo is served from a file with `no-store` instead of an inlined blob.

## Earlier

Pre-2.5 history is in the git log; the initial public import is the root of this repository.
5 changes: 5 additions & 0 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ Behind the curtain, a tiny local Python loop checks for changes about once a
minute. When nothing changed, no AI runs. When something did, it wakes one agent
for one target, records the result, and goes back to sleep.

Free Slack checking requires a workspace Slack app. If that is unavailable, set
`YAAS_SLACK_CHECKERS_ENABLED=0` in `.env`. Slack watch entries stay recorded but dormant,
the reaction sweep is disabled, and schedule, email, Jira, and GitHub watches continue.
Scheduled agent runs can still access Slack through the agent's own MCP connector.

That is the whole rhythm: **react for now, quest for later, review before send.**

Want the technical tour? See the full [Sidequestor README](README.md).
39 changes: 27 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

> *Safely expose your local interactive agent setup as a service.*

**Version 2.5.1** (see [`CHANGELOG.md`](CHANGELOG.md); the current version is also in `VERSION`).

**Sidequestor (YaaS, short for Yourself-as-a-Service)** keeps your local agent tending the work
you can't sit and watch: it turns the loose ends scattered across your channels into things your
own agent chases down while you're doing something else.
Expand Down Expand Up @@ -99,11 +101,12 @@ Attach https://github.com/circlefin/sidequestor.git to this repository. Follow i
existing-repository contract exactly, preserve all existing work, and stop on any unsafe collision.
```

Both prompts stop and ask you before Slack authorization and before background jobs are installed.
Both prompts stop and ask you before Slack authorization, when local Slack checking is enabled,
and before background jobs are installed.
Prefer the fresh folder unless Sidequestor has to live inside an existing repository. Manual
route and prerequisites: [Pack the kit](#pack-the-kit).

> **macOS only.** It runs on `launchd` and keeps its Slack token in the Keychain, so it will not
> **macOS only.** It runs on `launchd` and, when local Slack checking is enabled, keeps its Slack token in the Keychain, so it will not
> run as-is on Linux or Windows. Early project, no support commitment.

Sidequestor is built for solutions engineers, architects, PMs, support and ops leads, researchers,
Expand Down Expand Up @@ -181,15 +184,16 @@ is a confusing `TypeError` mid-dispatch, so `doctor.sh` checks the version expli
### What the install prompt does

You paste the prompt; the agent does the work. It clones, creates `.env` and your agent-rules
file, prints the Slack manifest, runs setup, installs the background jobs, and verifies the whole
file, prints the Slack manifest when local Slack checking is wanted, runs setup, installs the background jobs, and verifies the whole
thing with the doctor, the health monitor, and the test suite. It stops and asks before Slack
authorization, before installing background jobs, and before any commit.

Four things it cannot do for you. They are the whole of your install:

**1. Create the Slack app.** Sidequestor ships no shared Marketplace app on purpose: the polling
**1. Choose how Slack is checked.** Sidequestor ships no shared Marketplace app on purpose: the polling
loop needs a credential it can use without waking a model, and that credential should belong to
you rather than to a vendor. So the app has to exist in **your** workspace first.
you rather than to a vendor. For free per-minute Slack checking, the app has to exist in **your**
workspace first.

Your agent prints a manifest with every scope and the redirect URL already filled in. Take it to
<https://api.slack.com/apps> → **Create New App** → **From an app manifest**, pick your workspace,
Expand All @@ -199,12 +203,17 @@ Budget more than two minutes if your workspace restricts app installs. `reaction
`reactions:write` are the scopes most often held for admin review; without them the reaction
triggers do nothing. Grant them later and ask your agent to re-authorize.

**2. Approve the OAuth.** A browser window, once. PKCE, no client secret. The token lands in your
Keychain, never in the repo.
If you are not able to set up an organisation level Slack app, set `YAAS_SLACK_CHECKERS_ENABLED=0` in `.env`. Setup then skips
Slack OAuth. The Python loop leaves every `slack_*` watch and its watermark untouched, skips the
reaction sweep, and continues checking schedules and all non-Slack sources. A scheduled paid
worker may still read those Slack entries and access Slack through its own MCP connector.

**2. Approve the OAuth, if local Slack checking is enabled.** A browser window, once. PKCE, no
client secret. The token lands in your Keychain, never in the repo.

**3. Say what goes in `.env` beyond Slack.** Your sender identity for email replies, reaction emoji
overrides, and the spend ceilings. Everything except the four `SLACK_*` values has a default, and
your agent will walk you through the ones worth changing. Worth a glance at the ceilings.
**3. Say what goes in `.env`.** Choose whether local Slack checking is enabled, then configure
the adapters you use, your sender identity for email replies, reaction emoji overrides, and the
spend ceilings. Your agent will walk you through the values worth changing.

**4. Put your voice in the agent-rules file.** `CLAUDE.md` for Claude, `AGENTS.md` for Codex, or
the addendum merged into your harness's own project-rules file. Who your agent is, how it writes,
Expand Down Expand Up @@ -390,11 +399,17 @@ Once installed, there is nothing to do. It ticks every 60 seconds.
The pinned dashboard at `localhost:8877` is the whole interface. That's where drafts wait for you and every quest's messages are one click from Slack. A pulsing
pill appears when a worker is running: click it to watch the agent think in real time. Stuck
(misconfigured) or throttled (rate limited) quests sort to the top with a badge. **Control** is
the live map, **Audit** keeps prior approvals and dispatch runs, and **Field guide** in the header
explains quests and Slack reaction spells.
the live map, **Briefings** reads the canonical Markdown archive, **Audit** keeps prior approvals
and dispatch runs, and **Field guide** in the header explains quests and Slack reaction spells.

The dashboard is the map. The files remain the truth, and your agent is the one that reads them.

Briefings have one canonical home: `state/briefs/`. Every Markdown file in that directory is a
briefing; filenames are free-form labels rather than a schema. The dashboard sorts files by their
filesystem creation time and recognizes `morning`, `evening`, `weekly`, or `monthly` in a name only
as an optional display hint. Anything posted to Slack is a delivery copy rather than the source of
truth. Briefings stay outside quest folders because they can summarize work across several quests.

For anything the dashboard doesn't show, ask. Your agent knows the ops surface, so questions work
better than commands:

Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.5.1
Loading
Loading