jagc 0.1.0
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.
- package/.env.example +7 -0
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/defaults/AGENTS.md +84 -0
- package/defaults/SYSTEM.md +27 -0
- package/defaults/settings.json +3 -0
- package/deploy/launchd/com.jagc.server.plist +58 -0
- package/deploy/systemd/jagc-restart.service +6 -0
- package/deploy/systemd/jagc.env.example +23 -0
- package/deploy/systemd/jagc.path +12 -0
- package/deploy/systemd/jagc.service +24 -0
- package/dist/api-contracts-BPwE7cri.mjs +118 -0
- package/dist/cli/main.mjs +1008 -0
- package/dist/server/main.mjs +3533 -0
- package/docs/architecture.md +161 -0
- package/docs/auth.md +104 -0
- package/docs/future.md +57 -0
- package/docs/release.md +81 -0
- package/docs/testing.md +58 -0
- package/migrations/001_runs_and_ingest.sql +24 -0
- package/migrations/002_thread_sessions.sql +9 -0
- package/package.json +86 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# jagc architecture (current implementation)
|
|
2
|
+
|
|
3
|
+
This doc is the implementation snapshot (not design intent).
|
|
4
|
+
|
|
5
|
+
- Current operator-facing contract: [`README.md`](../README.md)
|
|
6
|
+
- Deferred/historical notes: [`docs/future.md`](./future.md)
|
|
7
|
+
|
|
8
|
+
## Implemented surface (v0)
|
|
9
|
+
|
|
10
|
+
### HTTP API
|
|
11
|
+
|
|
12
|
+
- Core run lifecycle: `GET /healthz`, `POST /v1/messages`, `GET /v1/runs/:run_id`
|
|
13
|
+
- OAuth broker: `GET /v1/auth/providers`, `POST /v1/auth/providers/:provider/login`, `GET /v1/auth/logins/:attempt_id`, `POST /v1/auth/logins/:attempt_id/input`, `POST /v1/auth/logins/:attempt_id/cancel`
|
|
14
|
+
- Runtime controls: `GET /v1/models`, `GET /v1/threads/:thread_key/runtime`, `PUT /v1/threads/:thread_key/model`, `PUT /v1/threads/:thread_key/thinking`, `DELETE /v1/threads/:thread_key/session`
|
|
15
|
+
|
|
16
|
+
### CLI
|
|
17
|
+
|
|
18
|
+
- `jagc health`
|
|
19
|
+
- `jagc message`
|
|
20
|
+
- `jagc run wait`
|
|
21
|
+
- `jagc auth providers`, `jagc auth login <provider>`
|
|
22
|
+
- `jagc new`, `jagc model list|get|set`, `jagc thinking get|set`
|
|
23
|
+
- Service lifecycle + diagnostics: `jagc install|status|restart|uninstall|doctor` (macOS launchd implementation, future Linux/Windows planned)
|
|
24
|
+
|
|
25
|
+
### Runtime/adapters
|
|
26
|
+
|
|
27
|
+
- Executors: `echo` (deterministic), `pi` (real agent)
|
|
28
|
+
- Telegram polling adapter (personal chats) with `/settings`, `/new`, `/model`, `/thinking`, `/auth`
|
|
29
|
+
- SQLite persistence (`runs`, ingest idempotency, `thread_sessions`)
|
|
30
|
+
- SQLite DB is configured in WAL mode with `foreign_keys=ON`, `synchronous=NORMAL`, and `busy_timeout=5000`
|
|
31
|
+
- Structured Pino JSON logging with component-scoped child loggers shared across server/runtime/adapters
|
|
32
|
+
- HTTP request completion/error events are emitted from Fastify hooks with request IDs and duration fields
|
|
33
|
+
- In-process run scheduler for dispatch/recovery (no external workflow engine)
|
|
34
|
+
- CI release gate runs in GitHub Actions via `pnpm release:gate` (typecheck + lint + test + build + package smoke)
|
|
35
|
+
|
|
36
|
+
## Workspace bootstrap
|
|
37
|
+
|
|
38
|
+
- Startup bootstraps `JAGC_WORKSPACE_DIR` (`~/.jagc` by default) with directory mode `0700`.
|
|
39
|
+
- Bootstrap creates default `SYSTEM.md`, `AGENTS.md`, and `settings.json` from repo templates when missing (never overwrites existing files).
|
|
40
|
+
- Bootstrap also seeds bundled `defaults/skills/**` and `defaults/extensions/**` files into the workspace when missing (never overwrites existing files).
|
|
41
|
+
- Default `settings.json` includes bootstrap pi packages (`pi-librarian`, `pi-subdir-context`) but remains user-editable after creation.
|
|
42
|
+
- Bootstrap also ensures workspace `.gitignore` has `.sessions/`, `auth.json`, `git/`, `jagc.sqlite`, `jagc.sqlite-shm`, and `jagc.sqlite-wal` entries.
|
|
43
|
+
|
|
44
|
+
## macOS service lifecycle (CLI-managed)
|
|
45
|
+
|
|
46
|
+
- `jagc install` writes a per-user launch agent at `~/Library/LaunchAgents/<label>.plist` (`com.jagc.server` by default), then `launchctl bootstrap` + `kickstart` starts the service.
|
|
47
|
+
- launchd runs `node <installed package>/dist/server/main.mjs` directly (no `pnpm` runtime dependency after install).
|
|
48
|
+
- launchd environment variables include `JAGC_WORKSPACE_DIR`, `JAGC_DATABASE_PATH`, `JAGC_HOST`, `JAGC_PORT`, `JAGC_RUNNER`, and optional `JAGC_TELEGRAM_BOT_TOKEN`.
|
|
49
|
+
- Logs default to `$JAGC_WORKSPACE_DIR/logs/server.out.log` and `server.err.log`.
|
|
50
|
+
- `jagc status` inspects launchd (`launchctl print`) and API health (`/healthz`).
|
|
51
|
+
- `jagc restart` issues `launchctl kickstart -k` and waits for `/healthz`.
|
|
52
|
+
- `jagc uninstall` removes the launch agent and unloads it; `--purge-data` additionally deletes the workspace directory.
|
|
53
|
+
|
|
54
|
+
## Request/execution flow
|
|
55
|
+
|
|
56
|
+
### 1) Message ingest (`POST /v1/messages`)
|
|
57
|
+
|
|
58
|
+
- `src/server/app.ts` validates payload.
|
|
59
|
+
- Header/body idempotency key mismatch returns `400`.
|
|
60
|
+
- `RunService.ingestMessage(...)` writes/gets run via `RunStore.createRun(...)`.
|
|
61
|
+
- Non-deduplicated runs are enqueued into the in-process run scheduler.
|
|
62
|
+
- Response is a normalized run envelope (`run_id`, `status`, `output`, `error`) with `202`.
|
|
63
|
+
|
|
64
|
+
### 2) Run dispatch and execution
|
|
65
|
+
|
|
66
|
+
- `LocalRunScheduler` dispatches runs in process.
|
|
67
|
+
- Scheduler deduplicates currently scheduled run IDs (`run_id`) and does not use an external queue/workflow engine.
|
|
68
|
+
- Scheduler serializes dispatch per `thread_key` (FIFO at dispatch boundary), while allowing different threads to dispatch concurrently.
|
|
69
|
+
- Scheduler calls `RunService.dispatchRunById(run_id)`, which starts background execution only if the run is still `running` and not already in-flight.
|
|
70
|
+
- `RunExecutor.execute(run)` returns structured `RunOutput` or throws.
|
|
71
|
+
- `RunService` emits run progress lifecycle events (`queued`, `started`, `succeeded`, `failed`) and forwards executor/session progress events when provided (for adapters/UIs).
|
|
72
|
+
- Service finalizes with `markSucceeded` / `markFailed`.
|
|
73
|
+
|
|
74
|
+
### 3) Run polling (`GET /v1/runs/:run_id`)
|
|
75
|
+
|
|
76
|
+
- Returns normalized run response.
|
|
77
|
+
- Failed runs include `error.message`.
|
|
78
|
+
|
|
79
|
+
## Durability + recovery
|
|
80
|
+
|
|
81
|
+
- Source of truth is SQLite run state (`runs.status`).
|
|
82
|
+
- `RunService.init()` performs:
|
|
83
|
+
- immediate scan of `runs.status='running'`
|
|
84
|
+
- periodic recovery pass (15s) to re-enqueue missing in-process work
|
|
85
|
+
- Recovery skips runs already in the local in-flight completion set.
|
|
86
|
+
- Scheduler deduplicates currently scheduled run IDs so ingest + recovery can race safely.
|
|
87
|
+
|
|
88
|
+
### Concurrency scope
|
|
89
|
+
|
|
90
|
+
- Run dispatch is in-process and single-server-process scoped.
|
|
91
|
+
- Same-thread turn ordering (`followUp` / `steer`) is enforced by per-thread `ThreadRunController` instances in the pi executor.
|
|
92
|
+
- Multi-process/global run coordination is intentionally deferred post-v0.
|
|
93
|
+
|
|
94
|
+
## Session/thread model (pi executor)
|
|
95
|
+
|
|
96
|
+
- Session identity is per `thread_key`.
|
|
97
|
+
- `thread_sessions` persists `thread_key`, `session_id`, `session_file`.
|
|
98
|
+
- `PiRunExecutor` reopens persisted sessions when possible; creates/persists when missing/invalid.
|
|
99
|
+
- In-memory session cache is hot-path only; SQLite mapping is source of truth across restarts.
|
|
100
|
+
|
|
101
|
+
### Same-thread coordination (non-obvious)
|
|
102
|
+
|
|
103
|
+
`ThreadRunController` coordinates same-thread turns against a single pi session:
|
|
104
|
+
|
|
105
|
+
- First active run uses `session.prompt(...)`.
|
|
106
|
+
- Additional same-thread runs queue via `session.followUp(...)` or `session.steer(...)`.
|
|
107
|
+
- Run completion attribution comes from session events (not prompt promise timing), using user/assistant boundary events.
|
|
108
|
+
|
|
109
|
+
Operational note:
|
|
110
|
+
|
|
111
|
+
- With the in-process scheduler feeding a per-thread controller, same-thread `followUp`/`steer` messages can be delivered while a session is active.
|
|
112
|
+
- If the process crashes, pending `running` rows are replayed from SQLite on recovery.
|
|
113
|
+
|
|
114
|
+
## Telegram polling behavior
|
|
115
|
+
|
|
116
|
+
- Ingest source: grammY long polling (personal chats).
|
|
117
|
+
- Thread mapping: `thread_key = telegram:chat:<chat_id>`.
|
|
118
|
+
- User mapping: `user_key = telegram:user:<from.id>`.
|
|
119
|
+
- Default delivery mode for normal text messages: `followUp` (`/steer` is explicit).
|
|
120
|
+
- Telegram `/new` and API `DELETE /v1/threads/:thread_key/session` abort/dispose the current thread session, clear persisted `thread_sessions` mapping, and cause the next message to create a fresh pi session.
|
|
121
|
+
- Adapter starts a per-run progress reporter (in-chat append-style progress message + typing indicator) as soon as a run is ingested.
|
|
122
|
+
- Progress is driven by run-level events emitted from `RunService` and pi session events forwarded by `ThreadRunController` (`assistant_text_delta`, `assistant_thinking_delta`, `tool_execution_*`, turn/agent lifecycle), rendered as compact append-log lines (`>` for tool calls with args-focused snippets, `~` for short thinking snippets).
|
|
123
|
+
- Status updates are edit-throttled and retry-aware for Telegram rate limits (`retry_after`).
|
|
124
|
+
- Adapter waits for terminal run status and replies with output/error.
|
|
125
|
+
- If foreground wait exceeds adapter timeout, Telegram receives a "still running" notice and the adapter continues waiting in the background, then posts final output when complete.
|
|
126
|
+
- `/model` and `/thinking` use button pickers; text args are intentionally unsupported.
|
|
127
|
+
- After model/thinking changes, the adapter returns to the `/settings` panel and shows the updated runtime state.
|
|
128
|
+
- The `/settings` panel does not include a dedicated refresh button; reopening `/settings` (or returning from a change) re-fetches live state.
|
|
129
|
+
- Outdated/invalid callback payloads trigger stale-menu recovery: the adapter re-renders the latest `/settings` panel.
|
|
130
|
+
- Telegram callback payload size is capped at 64 bytes; over-limit model/auth options are hidden and surfaced with an in-chat warning.
|
|
131
|
+
|
|
132
|
+
## OAuth broker + runtime controls
|
|
133
|
+
|
|
134
|
+
### OAuth broker
|
|
135
|
+
|
|
136
|
+
- Backed by `PiAuthService` + `OAuthLoginBroker`.
|
|
137
|
+
- Login attempts are owner-scoped via `X-JAGC-Auth-Owner`.
|
|
138
|
+
- Follow-up endpoints require owner header and return `404` on owner mismatch.
|
|
139
|
+
- Capacity behavior: if active attempts fill broker capacity, new starts return `429 auth_login_capacity_exceeded` (no eviction of active attempts).
|
|
140
|
+
- Successful credentials persist via pi `AuthStorage` to workspace `auth.json`.
|
|
141
|
+
|
|
142
|
+
### Runtime controls (model/thinking)
|
|
143
|
+
|
|
144
|
+
- `PiRunExecutor` is source of truth for per-thread runtime state.
|
|
145
|
+
- Model updates call `AgentSession.setModel(...)` (validated via pi `ModelRegistry`, persisted via `SettingsManager`).
|
|
146
|
+
- Thinking updates call `AgentSession.setThinkingLevel(...)` and return effective/clamped level + available levels.
|
|
147
|
+
- jagc does not duplicate model/thinking state in its own DB.
|
|
148
|
+
|
|
149
|
+
## Contracts + schema source of truth
|
|
150
|
+
|
|
151
|
+
- API schemas: `src/shared/api-contracts.ts` (used by server + CLI)
|
|
152
|
+
- Run progress event contract: `src/shared/run-progress.ts`
|
|
153
|
+
- Migrations: `migrations/001_runs_and_ingest.sql`, `migrations/002_thread_sessions.sql`
|
|
154
|
+
- Migration runner: `src/server/migrations.ts` (`schema_migrations`; startup apply runs in a SQLite `BEGIN IMMEDIATE` transaction to avoid concurrent bootstrap races)
|
|
155
|
+
|
|
156
|
+
## Known gaps / intentional limitations
|
|
157
|
+
|
|
158
|
+
- Telegram webhook mode is intentionally unsupported in core (polling is the only supported Telegram mode).
|
|
159
|
+
- Webhook hardening beyond current baseline is pending (signatures/replay protection).
|
|
160
|
+
- Linux/systemd and Windows service lifecycle commands are not implemented yet (macOS launchd is first supported target).
|
|
161
|
+
- Multi-process one-active-run-per-thread coordination is deferred.
|
package/docs/auth.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Authentication and model access (v0)
|
|
2
|
+
|
|
3
|
+
## Current behavior
|
|
4
|
+
|
|
5
|
+
jagc uses pi SDK auth resolution order for model credentials:
|
|
6
|
+
|
|
7
|
+
1. Runtime API key override (not used by jagc yet)
|
|
8
|
+
2. `auth.json` in `JAGC_WORKSPACE_DIR` (default `~/.jagc/auth.json`)
|
|
9
|
+
3. Provider environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.)
|
|
10
|
+
4. Custom provider fallback from `models.json`
|
|
11
|
+
|
|
12
|
+
`JAGC_WORKSPACE_DIR` is the single directory for both jagc workspace files and pi agent resources (skills, prompts, extensions, themes, settings, auth, sessions).
|
|
13
|
+
|
|
14
|
+
## Workspace bootstrap
|
|
15
|
+
|
|
16
|
+
On server startup, jagc ensures `JAGC_WORKSPACE_DIR` exists (mode `0700`), creates missing defaults (`SYSTEM.md`, `AGENTS.md`, `settings.json`), seeds bundled `defaults/skills/**` and `defaults/extensions/**` files when missing, and ensures workspace `.gitignore` contains:
|
|
17
|
+
|
|
18
|
+
- `.sessions/`
|
|
19
|
+
- `auth.json`
|
|
20
|
+
- `git/`
|
|
21
|
+
|
|
22
|
+
It **does not copy** `~/.pi/agent/settings.json` or `~/.pi/agent/auth.json`.
|
|
23
|
+
|
|
24
|
+
## Fast setup paths
|
|
25
|
+
|
|
26
|
+
### Path A: OAuth login via jagc (recommended for remote/headless)
|
|
27
|
+
|
|
28
|
+
Start from CLI:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
jagc auth providers --json
|
|
32
|
+
jagc auth login openai-codex
|
|
33
|
+
|
|
34
|
+
# optional: make retries/resume deterministic across terminals
|
|
35
|
+
jagc auth login openai-codex --owner-key cli:anton:laptop
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Start from Telegram:
|
|
39
|
+
|
|
40
|
+
- `/auth` opens provider picker
|
|
41
|
+
- tap a provider to start login
|
|
42
|
+
- if input is requested, send `/auth input <value>`
|
|
43
|
+
- `/auth status` refreshes the current attempt
|
|
44
|
+
|
|
45
|
+
### Path B: provide env vars to the jagc process
|
|
46
|
+
|
|
47
|
+
Set provider vars in the environment where the server runs (shell, launchd env, systemd env file).
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export OPENAI_API_KEY=...
|
|
53
|
+
export ANTHROPIC_API_KEY=...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For deployment, inject these into your service manager instead of interactive shells.
|
|
57
|
+
|
|
58
|
+
### Path C: manually manage `auth.json`
|
|
59
|
+
|
|
60
|
+
You can still pre-populate `JAGC_WORKSPACE_DIR/auth.json` using pi-compatible credentials.
|
|
61
|
+
|
|
62
|
+
## Discover what is missing
|
|
63
|
+
|
|
64
|
+
Use the auth/runtime status endpoints via CLI:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
jagc auth providers --json
|
|
68
|
+
jagc model list --json
|
|
69
|
+
jagc model get --thread-key cli:default --json
|
|
70
|
+
jagc thinking get --thread-key cli:default --json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This reports:
|
|
74
|
+
|
|
75
|
+
- per provider auth state (`has_auth`, credential type, env var hint, OAuth support)
|
|
76
|
+
- model catalog grouped by provider
|
|
77
|
+
- current thread model selection
|
|
78
|
+
- current thread thinking level and available thinking levels
|
|
79
|
+
|
|
80
|
+
## OAuth broker API (implemented)
|
|
81
|
+
|
|
82
|
+
Server endpoints:
|
|
83
|
+
|
|
84
|
+
- `POST /v1/auth/providers/:provider/login` — start OAuth login attempt (or return the active one for the same owner + provider)
|
|
85
|
+
- `GET /v1/auth/logins/:attempt_id` — inspect current attempt status
|
|
86
|
+
- `POST /v1/auth/logins/:attempt_id/input` — submit requested input (`prompt` or `manual_code`)
|
|
87
|
+
- `POST /v1/auth/logins/:attempt_id/cancel` — cancel attempt
|
|
88
|
+
|
|
89
|
+
Ownership/isolation rules:
|
|
90
|
+
|
|
91
|
+
- OAuth attempts are scoped by `X-JAGC-Auth-Owner`.
|
|
92
|
+
- `POST /login` accepts an optional owner header; if omitted, jagc generates one.
|
|
93
|
+
- Follow-up endpoints (`GET`, `/input`, `/cancel`) require `X-JAGC-Auth-Owner`.
|
|
94
|
+
- Attempt operations with the wrong owner return `404` to avoid cross-client/session leakage.
|
|
95
|
+
|
|
96
|
+
Attempt snapshots include:
|
|
97
|
+
|
|
98
|
+
- owner key + provider + attempt id
|
|
99
|
+
- current status (`running|awaiting_input|succeeded|failed|cancelled`)
|
|
100
|
+
- browser URL/instructions (when available)
|
|
101
|
+
- requested input prompt (when waiting for user input)
|
|
102
|
+
- progress messages and terminal error text
|
|
103
|
+
|
|
104
|
+
Successful logins are persisted to `auth.json` through pi `AuthStorage.login()` and are used immediately by `ModelRegistry`.
|
package/docs/future.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# jagc future (deferred + historical notes)
|
|
2
|
+
|
|
3
|
+
This file is intentionally short.
|
|
4
|
+
|
|
5
|
+
- **Current behavior/contracts** live in [`README.md`](../README.md) and [`docs/architecture.md`](./architecture.md).
|
|
6
|
+
- This file tracks only **post-v0 priorities** and a few **historical decisions** worth keeping visible.
|
|
7
|
+
- Removed pre-v0 draft detail can be recovered from git history if needed.
|
|
8
|
+
|
|
9
|
+
## v0 shipped baseline (context only)
|
|
10
|
+
|
|
11
|
+
v0 is shipped as pre-alpha with:
|
|
12
|
+
|
|
13
|
+
- Server: `/healthz`, `/v1/messages`, `/v1/runs/:run_id`, auth endpoints, model/runtime controls.
|
|
14
|
+
- CLI: `health`, `message`, `run wait`, auth/model/thinking commands, and macOS service lifecycle (`install|status|restart|uninstall|doctor`).
|
|
15
|
+
- Runtime: pi SDK in-process sessions with SQLite-backed run state + in-process scheduling.
|
|
16
|
+
- Concurrency: in-process dispatch + per-thread pi session turn control (`followUp` / `steer`); multi-process/global locking deferred.
|
|
17
|
+
- Telegram: polling adapter for personal chats + button-based `/settings` `/model` `/thinking` `/auth`.
|
|
18
|
+
|
|
19
|
+
Do not add implementation detail here unless it is deferred/future-looking.
|
|
20
|
+
|
|
21
|
+
## Post-v0 priorities
|
|
22
|
+
|
|
23
|
+
### P1 — high leverage
|
|
24
|
+
|
|
25
|
+
1. **Webhook hardening**
|
|
26
|
+
- Keep bearer-token baseline.
|
|
27
|
+
- Add per-source signature verification where available.
|
|
28
|
+
- Add replay protection (timestamp + nonce window).
|
|
29
|
+
|
|
30
|
+
2. **Release hardening follow-ups**
|
|
31
|
+
- Add automated policy checks for changelog section quality/content.
|
|
32
|
+
- Add optional staged/canary release flow before promoting to `latest`.
|
|
33
|
+
|
|
34
|
+
### P2 — operator/developer UX
|
|
35
|
+
|
|
36
|
+
1. **`jagc workspace init`** for quick workspace scaffolding.
|
|
37
|
+
2. **Explicit integration test target** (e.g., `pnpm test:integration`) distinct from unit tests.
|
|
38
|
+
3. **Service logs UX** (structured tail/filter command) to reduce launchctl/manual log digging.
|
|
39
|
+
|
|
40
|
+
### P3 — deployment maturity
|
|
41
|
+
|
|
42
|
+
1. Add first-class Linux (`systemd`) and Windows (`SCM`) implementations behind the same `jagc install|status|restart|uninstall` interface.
|
|
43
|
+
2. Add backup/restore guidance for workspace + SQLite before risky upgrades.
|
|
44
|
+
3. Add one-command operator backup verification smoke before upgrades.
|
|
45
|
+
|
|
46
|
+
## Near-term non-goals
|
|
47
|
+
|
|
48
|
+
- Telegram webhook runtime mode in core (polling remains the supported Telegram mode).
|
|
49
|
+
- Multi-host/distributed runtime orchestration.
|
|
50
|
+
- New ingress adapters in core beyond current v0 scope.
|
|
51
|
+
- Large API surface expansion before hardening current contracts.
|
|
52
|
+
|
|
53
|
+
## Historical decisions worth preserving
|
|
54
|
+
|
|
55
|
+
- Keep `output` as a structured payload contract (not plain text only).
|
|
56
|
+
- Keep same-thread default delivery mode as `followUp`; `steer` is explicit.
|
|
57
|
+
- Keep provider/model/thinking state in pi settings/session state; do not duplicate in jagc DB.
|
package/docs/release.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Release runbook
|
|
2
|
+
|
|
3
|
+
This is the canonical publish procedure for `jagc`.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
- Release channel: **latest** only (stable tags `vX.Y.Z`).
|
|
8
|
+
- Publish target: npm package **`jagc`**.
|
|
9
|
+
- Owner account: npm user **`akuzmenko`**.
|
|
10
|
+
|
|
11
|
+
## One-time setup
|
|
12
|
+
|
|
13
|
+
1. Ensure npm package ownership includes `akuzmenko`:
|
|
14
|
+
- `npm owner ls jagc`
|
|
15
|
+
2. Enable npm trusted publishing for this repository in npm package settings.
|
|
16
|
+
- Provider: GitHub Actions
|
|
17
|
+
- Repo: `<github-owner>/jagc`
|
|
18
|
+
3. Ensure GitHub Actions is enabled and can request OIDC tokens.
|
|
19
|
+
- Workflow must have `permissions: id-token: write`.
|
|
20
|
+
|
|
21
|
+
> 2FA remains enabled on the npm account. Trusted publishing avoids long-lived npm tokens in repo secrets.
|
|
22
|
+
|
|
23
|
+
### First-release bootstrap (only if needed)
|
|
24
|
+
|
|
25
|
+
If npm does not allow trusted publishing setup before the package exists, do one manual bootstrap publish from a trusted local machine:
|
|
26
|
+
|
|
27
|
+
1. `pnpm release:gate`
|
|
28
|
+
2. `npm login` (account: `akuzmenko`)
|
|
29
|
+
3. `npm publish --access public`
|
|
30
|
+
4. Configure trusted publishing for this repo.
|
|
31
|
+
5. All subsequent releases use tag-driven GitHub Actions only.
|
|
32
|
+
|
|
33
|
+
## Changelog format (required)
|
|
34
|
+
|
|
35
|
+
`CHANGELOG.md` must always contain:
|
|
36
|
+
|
|
37
|
+
- `## [Unreleased]`
|
|
38
|
+
- Version sections formatted as `## [X.Y.Z] - YYYY-MM-DD`
|
|
39
|
+
- Structured subsections (`Added`, `Changed`, `Fixed`, optional `Removed`/`Security`)
|
|
40
|
+
|
|
41
|
+
## Release steps
|
|
42
|
+
|
|
43
|
+
1. **Prepare release commit**
|
|
44
|
+
- Move completed entries from `## [Unreleased]` into a new section:
|
|
45
|
+
- `## [X.Y.Z] - YYYY-MM-DD`
|
|
46
|
+
- Reset `## [Unreleased]` back to placeholders.
|
|
47
|
+
- Bump package version in `package.json` to `X.Y.Z`.
|
|
48
|
+
|
|
49
|
+
2. **Validate locally**
|
|
50
|
+
- Run: `pnpm release:gate`
|
|
51
|
+
|
|
52
|
+
3. **Commit + merge to main**
|
|
53
|
+
- Commit changelog/version/docs updates.
|
|
54
|
+
- Merge PR to `main`.
|
|
55
|
+
|
|
56
|
+
4. **Tag release**
|
|
57
|
+
- `git tag -a vX.Y.Z -m "release: vX.Y.Z"`
|
|
58
|
+
- `git push origin vX.Y.Z`
|
|
59
|
+
|
|
60
|
+
5. **Automated publish**
|
|
61
|
+
- GitHub Actions `release` workflow runs on the tag.
|
|
62
|
+
- Workflow verifies tag/version/changelog consistency.
|
|
63
|
+
- Workflow runs `pnpm release:gate`.
|
|
64
|
+
- Workflow publishes with:
|
|
65
|
+
- `npm publish --access public --provenance`
|
|
66
|
+
- Workflow creates/updates GitHub release notes from the matching changelog section.
|
|
67
|
+
|
|
68
|
+
6. **Post-release verification**
|
|
69
|
+
- `npm view jagc version dist-tags --json`
|
|
70
|
+
- Fresh install smoke:
|
|
71
|
+
- `npm install -g jagc@latest`
|
|
72
|
+
- `jagc --help`
|
|
73
|
+
|
|
74
|
+
## Failure + rollback
|
|
75
|
+
|
|
76
|
+
- If publish fails before npm upload: fix and re-run workflow.
|
|
77
|
+
- If a bad version is published:
|
|
78
|
+
1. Deprecate bad version:
|
|
79
|
+
- `npm deprecate jagc@X.Y.Z "broken release; upgrade to >=X.Y.Z+1"`
|
|
80
|
+
2. Cut hotfix `X.Y.Z+1` via same runbook.
|
|
81
|
+
- Do **not** unpublish stable versions except for exceptional legal/security cases.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Testing strategy
|
|
2
|
+
|
|
3
|
+
## Database feedback loop (SQLite)
|
|
4
|
+
|
|
5
|
+
DB-backed tests use `tests/helpers/sqlite-test-db.ts`:
|
|
6
|
+
|
|
7
|
+
- one in-memory SQLite database per test file
|
|
8
|
+
- migrations applied once in `beforeAll`
|
|
9
|
+
- table reset (`DELETE`) before each test for deterministic isolation
|
|
10
|
+
- no transactional test harness (`BEGIN/ROLLBACK`) and no external DB process
|
|
11
|
+
|
|
12
|
+
This keeps tests fast, parallel-friendly, and hermetic under Vitest worker file parallelism.
|
|
13
|
+
|
|
14
|
+
## Telegram adapter feedback loop
|
|
15
|
+
|
|
16
|
+
Telegram tests use a local behavioral Bot API clone (`tests/helpers/telegram-bot-api-clone.ts`) instead of manual `grammY` context mocks.
|
|
17
|
+
|
|
18
|
+
Primary coverage lives in:
|
|
19
|
+
|
|
20
|
+
- shared harness: `tests/helpers/telegram-test-kit.ts` (common bot token/chat fixtures, adapter+clone lifecycle helper, thread control fake)
|
|
21
|
+
- `tests/telegram-polling-message-flow.test.ts` (plain text, `/steer`, append-log progress + typing indicator behavior, `>`/`~` stream rendering, tool-argument snippet rendering, completion states, timeout/background completion handoff, long-output chunking, and adapter-level recovery from transient polling errors)
|
|
22
|
+
- `tests/telegram-runtime-controls.test.ts` (settings/model/thinking/auth callback flows)
|
|
23
|
+
- `tests/telegram-polling.test.ts` (command/callback parsing and stale callback recovery)
|
|
24
|
+
- `tests/telegram-bot-api-clone.test.ts` (clone contract edges: `allowed_updates`/offset semantics, transient `getUpdates` error retry compatibility (`500`/`429 retry_after`), malformed payload handling, and urlencoded payload parsing)
|
|
25
|
+
- `tests/telegram-system-smoke.test.ts` (system-level smoke: real run service + scheduler + SQLite + Fastify app + polling adapter + clone)
|
|
26
|
+
- `tests/cli-service-manager.test.ts` (launchd service-manager helpers: plist rendering, server entrypoint resolution, launchctl output parsing)
|
|
27
|
+
|
|
28
|
+
This clone is intentionally narrow: it only implements the polling and messaging surface that jagc uses in v0:
|
|
29
|
+
|
|
30
|
+
- `getMe`
|
|
31
|
+
- `getUpdates` (including `offset`, `limit`, `allowed_updates`, and long-poll timeout behavior)
|
|
32
|
+
- `sendMessage`
|
|
33
|
+
- `editMessageText`
|
|
34
|
+
- `sendChatAction`
|
|
35
|
+
- `answerCallbackQuery`
|
|
36
|
+
|
|
37
|
+
### Why
|
|
38
|
+
|
|
39
|
+
The goal is to test real adapter behavior through grammY's network stack and long-polling loop, not private handler internals.
|
|
40
|
+
|
|
41
|
+
That gives us stable refactors and catches protocol-shape regressions that context stubs cannot.
|
|
42
|
+
|
|
43
|
+
### Scope rules for the clone
|
|
44
|
+
|
|
45
|
+
- Keep this clone a **contract clone**, not a full Telegram reimplementation.
|
|
46
|
+
- Only add Bot API methods when jagc starts using them.
|
|
47
|
+
- Keep method behavior deterministic and assertion-friendly.
|
|
48
|
+
- Record outbound bot calls so tests assert on real API payloads (`text`, `reply_markup.inline_keyboard`, callback answers).
|
|
49
|
+
- Accept both JSON and `application/x-www-form-urlencoded` Bot API payloads (including JSON-encoded nested fields like `reply_markup`) to stay resilient to client transport changes.
|
|
50
|
+
- Fail loud on malformed request payloads (invalid JSON or invalid `getUpdates` argument shapes) to avoid silent transport bugs in tests.
|
|
51
|
+
|
|
52
|
+
### Commands
|
|
53
|
+
|
|
54
|
+
- Full non-smoke suite (includes Telegram behavioral tests): `pnpm test`
|
|
55
|
+
- Focused Telegram suite (optional while iterating, includes Telegram system smoke): `pnpm test:telegram`
|
|
56
|
+
- npm package smoke (pack + install + run from tarball): `pnpm test:pack`
|
|
57
|
+
- macOS launchd service smoke (manual): `jagc install --runner echo --port <port> && jagc status && jagc doctor && jagc uninstall`
|
|
58
|
+
- Local release gate: `pnpm release:gate` (equivalent to `pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm test:pack`)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
2
|
+
run_id TEXT PRIMARY KEY,
|
|
3
|
+
source TEXT NOT NULL,
|
|
4
|
+
thread_key TEXT NOT NULL,
|
|
5
|
+
user_key TEXT,
|
|
6
|
+
delivery_mode TEXT NOT NULL DEFAULT 'followUp' CHECK (delivery_mode IN ('steer', 'followUp')),
|
|
7
|
+
status TEXT NOT NULL CHECK (status IN ('running', 'succeeded', 'failed')),
|
|
8
|
+
input_text TEXT NOT NULL,
|
|
9
|
+
output TEXT,
|
|
10
|
+
error_message TEXT,
|
|
11
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
12
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS message_ingest (
|
|
16
|
+
source TEXT NOT NULL,
|
|
17
|
+
idempotency_key TEXT NOT NULL,
|
|
18
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id),
|
|
19
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
20
|
+
PRIMARY KEY (source, idempotency_key)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX IF NOT EXISTS runs_thread_status_idx ON runs (thread_key, status);
|
|
24
|
+
CREATE INDEX IF NOT EXISTS runs_created_at_idx ON runs (created_at);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS thread_sessions (
|
|
2
|
+
thread_key TEXT PRIMARY KEY,
|
|
3
|
+
session_id TEXT NOT NULL,
|
|
4
|
+
session_file TEXT NOT NULL,
|
|
5
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
6
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
CREATE INDEX IF NOT EXISTS thread_sessions_session_id_idx ON thread_sessions (session_id);
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jagc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "just a good clanker",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/default-anton/jagc.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/default-anton/jagc/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/default-anton/jagc#readme",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"tag": "latest"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"jagc": "dist/cli/main.mjs"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"defaults/AGENTS.md",
|
|
28
|
+
"defaults/SYSTEM.md",
|
|
29
|
+
"defaults/settings.json",
|
|
30
|
+
"migrations",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"docs/architecture.md",
|
|
35
|
+
"docs/auth.md",
|
|
36
|
+
"docs/testing.md",
|
|
37
|
+
"docs/future.md",
|
|
38
|
+
"docs/release.md",
|
|
39
|
+
"deploy/launchd/com.jagc.server.plist",
|
|
40
|
+
"deploy/systemd/*.service",
|
|
41
|
+
"deploy/systemd/*.path",
|
|
42
|
+
"deploy/systemd/*.example",
|
|
43
|
+
".env.example"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"dev": "tsx watch src/server/main.ts",
|
|
47
|
+
"dev:cli": "tsx src/cli/main.ts",
|
|
48
|
+
"smoke": "scripts/smoke.sh",
|
|
49
|
+
"build": "tsdown src/server/main.ts src/cli/main.ts --format esm --out-dir dist",
|
|
50
|
+
"prepack": "pnpm build",
|
|
51
|
+
"start": "node dist/server/main.mjs",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:telegram": "vitest run tests/telegram-bot-api-clone.test.ts tests/telegram-polling.test.ts tests/telegram-polling-message-flow.test.ts tests/telegram-runtime-controls.test.ts tests/telegram-system-smoke.test.ts",
|
|
54
|
+
"test:pack": "scripts/pack-smoke.sh",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"lint": "biome check .",
|
|
57
|
+
"format": "biome check --write .",
|
|
58
|
+
"release:verify-tag": "node scripts/verify-release-tag.mjs",
|
|
59
|
+
"release:notes": "node scripts/changelog-release-notes.mjs",
|
|
60
|
+
"release:gate": "pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm test:pack"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@grammyjs/runner": "^2.0.3",
|
|
64
|
+
"@mariozechner/pi-coding-agent": "^0.52.7",
|
|
65
|
+
"better-sqlite3": "^12.4.1",
|
|
66
|
+
"commander": "^14.0.3",
|
|
67
|
+
"fastify": "^5.7.4",
|
|
68
|
+
"grammy": "^1.38.3",
|
|
69
|
+
"pino": "^10.3.0",
|
|
70
|
+
"zod": "^4.3.6"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@biomejs/biome": "^2.3.14",
|
|
74
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
75
|
+
"@types/node": "^25.2.1",
|
|
76
|
+
"tsdown": "^0.20.3",
|
|
77
|
+
"tsx": "^4.21.0",
|
|
78
|
+
"typescript": "^5.9.3",
|
|
79
|
+
"vitest": "^4.0.18"
|
|
80
|
+
},
|
|
81
|
+
"pnpm": {
|
|
82
|
+
"onlyBuiltDependencies": [
|
|
83
|
+
"better-sqlite3"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
}
|