gadak is a local-first mirror of an issue tracker. The binary stays free of your company’s CD pipeline, test manager, and org chart. You extend it in three places, each with a different cost and power trade-off.
If you only need “how do I attach GitHub PRs?”, jump to
Five-minute quick start. For the enrichment table
contract itself, see PLUGINS.md.
| Axis | Where | When to use | Network on keystroke path? |
|---|---|---|---|
| Config | ~/.gadak/config.json (or Settings UI) |
Map custom fields, define team buckets, turn surfaces on/off, allowlist editable fields | No — applied in-process |
| Enrichments | External process writes SQLite enrichments |
Attach data from systems gadak must not import (GitHub, CD, QA tools, spreadsheets) | No — plugin runs on its own schedule |
| Direct SQL | gadak sql, agents, reports |
Ad-hoc questions, weekly digests, automations that read the mirror | No — reads the local file |
Full reference (defaults, floors, apply timing, hand-edit-only list):
CONFIGURATION.md.
| Key | Purpose |
|---|---|
fields |
FieldSpec[] (alias, ids, role, kind). Synced into list rows under the alias. Empty kind is display-only; a set kind enters the inline-edit allowlist. Auto-filled by gadak fields --apply / first full sync. |
bodyFields |
Extra ADF custom field ids folded into FTS body text. |
groupRules |
Ordered rules (projects / labels / components) → group id for team views. |
groupQuery |
Optional SELECT/WITH returning (issue key, group) for classification that does not fit those three lists. Runs when the derived view is rebuilt, not on a keystroke. Empty group = unclassified; NULL / missing key falls through to groupRules, then the assignee's member group. Site-specific CASE belongs here. |
groupLabels / groupColors / productByGroup |
Display names and colors for those groups. |
members |
Static member directory (avatar, group, account id) merged into bootstrap. |
features |
Feature flags: feed, push, deploy, qa, teamGroups (feed defaults on when omitted; others off). |
qaDashboardUrl |
Optional link shown next to QA surfaces. |
staleThresholdHours |
Hours in status before an open issue counts as stale (0 → UI default 72). |
syncIntervalSec |
Incremental sync period in seconds (0 → 60). Min 15 when set. Restart serve. |
reconcileIntervalSec |
Deletion reconcile period in seconds (0 → 3600). Min 300 when set. Restart serve. |
notify |
OS desktop notifications from the watch loop (default true). |
confluence.spaces |
Space allowlist. The list is the scope: drop a space and the next Confluence pass removes it; add one and that space is fetched from the start. Empty means every global space; personal spaces only if named. |
Nothing installation-specific belongs in the gadak source tree. Put it here.
A separate process upserts:
INSERT INTO enrichments (key, kind, payload, source, updated_at) VALUES (…)
ON CONFLICT(key, kind) DO UPDATE SET …;
UPDATE sync_state SET version = version + 1; -- required for UI refreshKnown kinds the UI merges today: deploy, qa, prs, opinion. Details and
payload tables: PLUGINS.md. Runnable examples:
examples/plugins/.
The mirror is a normal SQLite file. Agents and humans query it with
gadak sql (read-only). Cookbook queries live in
docs/MIRROR.md and docs/AGENT_ACCESS.md.
Never write mirrored issue fields by hand — the next sync overwrites them.
The only supported external write table is enrichments (plus personal tables
like watches/favorites via the API).
| We want… | Axis | How |
|---|---|---|
| A custom field on the list / filters | Config | Pin it in fields (Settings → Fields) or run gadak fields --apply. |
| “My team’s board” without Jira boards | Config | groupRules + features.teamGroups. |
| Classification that needs regex, exclusions, custom fields | Config | groupQuery — one SQL statement over issues_full / json_each. Do not grow groupRules. |
| Inline edit of a few fields | Config | Set kind on the spec in fields + stored credential. Empty kind is display-only. |
| Deploy badge / “is it in prod?” | Enrichment | kind=deploy — start from deploy-status. |
| PR links on the issue | Core / Enrichment | Core: dev_links / gadak dev. Plugin kind=prs is extra metadata — github-prs. |
| QA impact column | Enrichment | kind=qa + features.qa (shape in PLUGINS.md). |
| A free-text review note | Enrichment | kind=opinion (JSON string payload). |
| Spreadsheet-managed extras | Enrichment | csv-import. |
| Weekly reopen / load report | SQL | gadak sql + cron; see AGENTS.md cookbook. |
| “Has anyone hit this before?” | SQL | FTS via items_fts / gadak search. |
| Agent that comments or transitions | CLI / REST | gadak comment / gadak transition (writes go through the origin, then re-mirror). |
Prerequisites: Python 3.9+, sqlite3 CLI, a copy of the demo mirror (or your
own gadak.db).
# 0. Disposable DB
cp examples/demo.db /tmp/gadak-plugin.db
sqlite3 /tmp/gadak-plugin.db "SELECT version FROM sync_state;"
# 1. PRs from a fixture (no GitHub token)
python3 examples/plugins/github-prs/github_prs.py example/app \
--db /tmp/gadak-plugin.db \
--from-json examples/plugins/github-prs/sample-prs.json
# 2. Mixed rows from a spreadsheet
python3 examples/plugins/csv-import/csv_import.py \
examples/plugins/csv-import/sample.csv \
--db /tmp/gadak-plugin.db
# 3. Deploy stages from git tags (use your product repo, or --self-test)
python3 examples/plugins/deploy-status/deploy_status.py --self-test
# 4. Confirm
sqlite3 /tmp/gadak-plugin.db <<'SQL'
SELECT kind, COUNT(*) AS n FROM enrichments GROUP BY kind;
SELECT key, kind, substr(payload,1,100) FROM enrichments ORDER BY key, kind;
SELECT version FROM sync_state;
SQLTurn on surfaces in config when serving:
{ "features": { "deploy": true, "qa": true } }Live GitHub:
export GH_TOKEN=… # read-only is enough
python3 examples/plugins/github-prs/github_prs.py owner/repo --db ~/.gadak/gadak.dbMore detail per plugin: examples/plugins/README.md.
# Every 15 minutes — PRs
*/15 * * * * GH_TOKEN=… /usr/bin/python3 /opt/gadak-plugins/github_prs.py myorg/app --db /home/you/.gadak/gadak.db >>/var/log/gadak-prs.log 2>&1
# Hourly — deploy from a maintained mirror clone
0 * * * * /usr/bin/python3 /opt/gadak-plugins/deploy_status.py /srv/product.git --db /home/you/.gadak/gadak.db >>/var/log/gadak-deploy.log 2>&1~/Library/LaunchAgents/com.example.gadak-prs.plist → StartInterval 900,
ProgramArguments pointing at the same python invocation, EnvironmentVariables
for GH_TOKEN / GADAK_HOME.
gadak-prs.service (Type=oneshot) + gadak-prs.timer (OnUnitActiveSec=15m).
-
Did the plugin write?
gadak status --json gadak sql "SELECT kind, COUNT(*) FROM enrichments GROUP BY kind" gadak sql "SELECT key, kind, source, updated_at FROM enrichments ORDER BY updated_at DESC LIMIT 10"
-
Was
sync_state.versionbumped? If the row is on disk but the client still shows old data, the plugin forgot the version bump. Re-run a correct writer, or once:UPDATE sync_state SET version = version + 1;
-
Is the feature flag on?
features.deploy/features.qadefault to off. Check Settings orconfig.json. -
Is the JSON valid? Invalid payloads are dropped at read time with no error (so a bad plugin cannot corrupt the API). Validate before insert;
csv-importrejects bad JSON with a row number. -
Is the key present in the mirror? Enrichments for unknown keys are stored but never joined into list/detail until that issue is mirrored.
-
Stale watermark?
gadak status --json→last_errormeans the last issue-source sync failed (Jira, or Linear when that is the only issue source that has run; per-source rows aresources.jira/sources.linear);watermarkonly moves when the tracker changes.
- Enrichments cannot overwrite mirrored fields. The server serializes plugin
keys before issue fields;
status,summary, and friends always win. - Payload is opaque JSON per kind. The server does not validate beyond
json.Valid. Shape is a contract with the UI (web/src/lib/types.ts). - New kinds need a core PR. Adding
kind=security(or any surface not in the merge switch) requires server + UI work. Until then the row sits unused (or usecsv-import --allow-unknown-kindonly as a private holding area). - Enrichments are disposable. Deleting the DB and re-syncing drops them. The source system remains the record; re-run the plugin.
- Zero external integration code in this repository. Do not propose a
GitHub/Slack/Jenkins client inside
internal/. Ship a plugin process instead — like the examples underexamples/plugins/. Verified 2026-08-06:grep -R --include='*.go' -E 'go-github|slack-go|andygrunwald|xanzy/go-gitlab' internal/ cmd/→ empty. - Credentials stay out of SQLite. Tokens belong in the environment or a
secret store the plugin reads; never in
enrichments.payload.
PLUGINS.md— table schema, kind payload tables, example SQLAGENT_ACCESS.md— querying the mirror from agents../specs/000-product/data-model.md— full schema contract../examples/plugins/— runnable reference implementations