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
23 changes: 23 additions & 0 deletions .beads/interactions.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{"id":"int-ff04f17d106322290e4268157600a17a","kind":"field_change","created_at":"2026-07-22T14:56:04.299295Z","actor":"Sukumar Yethadka","issue_id":"jai-3xx","extra":{"field":"priority","new_value":"0","old_value":"2"}}
{"id":"int-35c2bd06150edfe2a73260097bf66df3","kind":"field_change","created_at":"2026-07-22T14:56:04.66326Z","actor":"Sukumar Yethadka","issue_id":"jai-z8o","extra":{"field":"priority","new_value":"0","old_value":"2"}}
{"id":"int-c2fd3057f93f87947927bb4fd39fff61","kind":"field_change","created_at":"2026-07-22T14:56:05.048303Z","actor":"Sukumar Yethadka","issue_id":"jai-wmz","extra":{"field":"priority","new_value":"1","old_value":"2"}}
{"id":"int-cbb618ce2fb727dcf0a70b1fda341a5e","kind":"field_change","created_at":"2026-07-22T14:56:05.418541Z","actor":"Sukumar Yethadka","issue_id":"jai-27b","extra":{"field":"priority","new_value":"3","old_value":"2"}}
{"id":"int-0698e1bfc756e55ec5bd8c7470846139","kind":"field_change","created_at":"2026-07-22T14:56:05.815648Z","actor":"Sukumar Yethadka","issue_id":"jai-d3j","extra":{"field":"priority","new_value":"3","old_value":"2"}}
{"id":"int-75ef2f586c96248d241c1be8405701ad","kind":"field_change","created_at":"2026-07-22T15:04:54.506249Z","actor":"Sukumar Yethadka","issue_id":"jai-3xx","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-1a14742a34596a838ca91cc53288d291","kind":"field_change","created_at":"2026-07-22T15:05:00.843542Z","actor":"Sukumar Yethadka","issue_id":"jai-wmz","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-8afb830acb5d700ae83610763e604999","kind":"field_change","created_at":"2026-07-22T15:06:43.29617Z","actor":"Sukumar Yethadka","issue_id":"jai-wmz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"SUMMARY: Added jai transition CLI command with case-insensitive name resolution, --list flag, ambiguous match detection, immediate auto-push via Writer.ProcessQueue(), and JSON output support. All 93 tests pass.. FILES: internal/cli/transition.go,internal/cli/transition_test.go. CONTEXT: 25%"}}
{"id":"int-72704933fea17cb46ecb2658f53f6416","kind":"field_change","created_at":"2026-07-22T15:07:00.766742Z","actor":"Sukumar Yethadka","issue_id":"jai-3xx","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"SUMMARY: Added --add and --remove flags to jai set for array fields. Validates field type via FieldMapping.Type, queues update_field operations with add/remove op, writer dispatches to new UpdateFieldOp Jira client method using update API format. Optimistic local update reads current JSON array, applies ops, writes back. 8 unit tests for applyArrayOps covering add, remove, duplicates, simultaneous ops.. FILES: internal/cli/set.go,internal/sync/writer.go,internal/jira/write.go,internal/cli/set_test.go. CONTEXT: 30%"}}
{"id":"int-4211fbb5c48427ad87582402de927b45","kind":"field_change","created_at":"2026-07-22T15:08:00.912028Z","actor":"Sukumar Yethadka","issue_id":"jai-z8o","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-21cd5eb358247773d03f9f4d9f939121","kind":"field_change","created_at":"2026-07-22T15:08:04.914611Z","actor":"Sukumar Yethadka","issue_id":"jai-d3j","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-34f515a2b2d5d31add9acdcdb8949121","kind":"field_change","created_at":"2026-07-22T15:09:43.545921Z","actor":"Sukumar Yethadka","issue_id":"jai-d3j","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"SUMMARY: Added jai link command with CreateLink and GetLinkTypes Jira client methods. Command supports default Relates type, --type flag, --list-types, and JSON output. Direct API call, not queued. All 106 tests pass.. FILES: internal/cli/link.go,internal/cli/link_test.go,internal/jira/write.go. CONTEXT: 25%"}}
{"id":"int-1bbf2e829f40c6d8f08c7985b4a3cc55","kind":"field_change","created_at":"2026-07-22T15:10:18.305615Z","actor":"Sukumar Yethadka","issue_id":"jai-z8o","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"SUMMARY: Fixed array field serialization in set command. When fieldType is array, setScalarField now splits comma-separated input via parseArrayValue, stores the value as a JSON array in pending_changes payload (using map[string]interface{} instead of map[string]string), and stores JSON-marshaled array for the optimistic local DB update. Writer processSetField already uses interface{} for Value so it sends the array correctly to Jira. Added tests for parseArrayValue and round-trip serialization.. FILES: internal/cli/set.go,internal/cli/set_test.go. CONTEXT: 25%"}}
{"id":"int-1827d557286be7d7fdbac00c189d91da","kind":"field_change","created_at":"2026-07-22T15:11:06.972043Z","actor":"Sukumar Yethadka","issue_id":"jai-27b","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-f1c69f67e2ccd81f79d9256e192da21f","kind":"field_change","created_at":"2026-07-22T15:14:52.477697Z","actor":"Sukumar Yethadka","issue_id":"jai-27b","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"SUMMARY: Added --query flag and comma-separated key expansion to jai set for bulk operations. --query runs SQL via g.query.Execute, extracts key column, queues pending changes for all matching issues. Comma-separated keys (ROX-1,ROX-2) are split and expanded. Bulk mode prints summary count (human) or {count,keys} JSON. Zero-row queries produce clear error. Unit tests for expandKeys and extractKeys.. FILES: internal/cli/set.go. CONTEXT: 30%"}}
{"id":"int-e340284f611af6fb7cc79524dc4e5899","kind":"field_change","created_at":"2026-07-23T07:28:45.63826Z","actor":"Sukumar Yethadka","issue_id":"jai-j1d","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-30a9376439036d3c0bc830aa1547e27e","kind":"field_change","created_at":"2026-07-23T07:30:48.986332Z","actor":"Sukumar Yethadka","issue_id":"jai-j1d","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed in 6409a6d: create.go and clone.go now resolve assignee emails to account IDs via jira.ResolveAccountID, same fix pattern as watch/unwatch (b8db35d). Verified against staging."}}
{"id":"int-21a68b7a0a1bd187b1ec35c7cf451612","kind":"field_change","created_at":"2026-07-23T08:35:34.103994Z","actor":"Sukumar Yethadka","issue_id":"jai-51m","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Fixed in 545dc35: added wrapScalarFieldValue/wrapArrayItemValue in internal/cli/set.go to shape priority/assignee/reporter/components/fixVersions correctly for Jira's write API, plus made UpdateFieldOp's value param interface{} to carry object values for --add/--remove. Verified against staging: priority, assignee, and components all set and pushed correctly."}}
{"id":"int-4df70fffe389bd55a80de4fd2b227bf4","kind":"field_change","created_at":"2026-07-23T08:35:34.788082Z","actor":"Sukumar Yethadka","issue_id":"jai-nv0","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Fixed in 545dc35 (same commit as jai-51m): setBulk now runs the same optimistic local DB UPDATE that the single-key path already did, for both scalar and --add/--remove bulk operations. Verified against staging: bulk set on 2 issues updated local DB immediately."}}
{"id":"int-bf1bf4026e45868886491a50ae3149fb","kind":"field_change","created_at":"2026-07-23T08:35:35.336914Z","actor":"Sukumar Yethadka","issue_id":"jai-jjy","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Fixed in 627f798: transition.go now re-fetches and re-denormalizes the issue after a successful transition push, mirroring the pattern jai clone already uses. Verified against staging: status reflects immediately after transition without a manual sync."}}
{"id":"int-3d641b17b7d7b329df145f8595df72cb","kind":"field_change","created_at":"2026-07-23T08:35:35.821812Z","actor":"Sukumar Yethadka","issue_id":"jai-1m4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Fixed in b04e492: added projectKeysFromJQL best-effort fallback in internal/query/engine.go for sync sources defined via jql: instead of an explicit projects: list. Verified against staging: {{projects}} now expands to 'ROX' using the real staging.yaml."}}
{"id":"int-37b9441dfd4e789c6df8faf5ddfd6374","kind":"field_change","created_at":"2026-07-27T12:12:59.742155Z","actor":"Sukumar Yethadka","issue_id":"jai-ijr","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-6969fc45fbe4bae456922ab3d887b86b","kind":"field_change","created_at":"2026-07-27T12:14:12.934354Z","actor":"Sukumar Yethadka","issue_id":"jai-ijr","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed: reuse stored DB name for existing fields instead of re-inferring from Jira metadata"}}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ jai
# Beads / Dolt files (added by bd init)
.beads-credential-key
.beads/proxieddb/

# TreeFlow context (auto-added)
.beads/
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ When working in any codebase with LSP support (Go, Python, Rust, TypeScript, etc
**Key architectural decisions**:
- DB-first: SQLite is the single source of truth; no command hits Jira API directly for reads
- All fields downloaded as raw JSON and denormalized into queryable columns — no resync needed for new fields
- Write operations queue locally in `pending_changes` table, synced to Jira via `jai push` or background goroutine
- Write operations push to Jira immediately by default; use `--queue` to defer to `pending_changes` table and `jai push`
- Custom field names auto-discovered from Jira's field metadata API and stored in `field_map` table
- FTS5 virtual table (`issues_fts`) with porter unicode61 tokenizer for full-text search
- WAL mode + pragmas for concurrent read/write performance
Expand All @@ -93,7 +93,7 @@ internal/
- Phase 1: Foundation — sync, query, get (core data loop)
- Phase 2: Agent Mode — --json, --fields, jai schema, jai fields, auto-sync
- Phase 3: TUI — full-screen views, sorting, filtering, grouping, background sync
- Phase 4: Write Path — jai set, jai comment, jai push, pending_changes
- Phase 4: Write Path — jai set, jai comment, jai push, write-through default with --queue opt-in
- Phase 5: Polish — jai init wizard, FTS5 search, color rules, default views, error UX
- Phase 6: Release — README, Homebrew, CI/CD, vhs demo

Expand Down
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jai takes a different approach: sync once to a local SQLite database, then query
- **10–50× fewer tokens** for AI agents. Select exactly the fields you need.
- **Instant queries** from local SQLite. No waiting for the API.
- **Full SQL power** — JOINs, aggregations, CTEs, window functions, FTS5. Things JQL will never do.
- **Works offline.** Writes queue locally and sync to Jira when you're back online.
- **Write-through by default.** Changes push to Jira immediately. Use `--queue` / `-q` to defer writes for offline use.
- **Full-screen TUI** that replaces the Jira web UI for daily workflows.

---
Expand Down Expand Up @@ -291,7 +291,7 @@ views:

## Write operations

Changes queue locally and sync to Jira on the next `jai push` or background sync cycle.
Changes push to Jira immediately by default. Use `--queue` / `-q` to defer changes for the next `jai push` or background sync cycle.

```sh
# Create a new issue (hits Jira API directly, returns key immediately)
Expand Down Expand Up @@ -327,21 +327,25 @@ jai clone ROX-4821 --replace "production:staging"

# Update a field
jai set ROX-4821 priority High
# → ROX-4821: priority → "High" (pending sync)
# → ROX-4821: priority → "High"

# Array fields — add/remove individual values
jai set ROX-4821 labels --add backend --add auth
# → ROX-4821: labels += [backend auth] (pending sync)
# → ROX-4821: labels += [backend auth]
jai set ROX-4821 labels --remove backend
# → ROX-4821: labels -= [backend] (pending sync)
# → ROX-4821: labels -= [backend]

# Bulk set — comma-separated keys or SQL query
jai set ROX-1,ROX-2,ROX-3 priority Major
# → queued 3 changes (pending sync)
# → 3 changes pushed ✓
jai set --query "SELECT key FROM issues WHERE type = 'Bug' LIMIT 5" priority Major
# → queued 5 changes (pending sync)
# → 5 changes pushed ✓

# Transition an issue (pushes immediately)
# Use --queue to defer changes instead of pushing immediately
jai set ROX-4821 priority High --queue
# → ROX-4821: priority → "High" (queued)

# Transition an issue
jai transition ROX-4821 "In Progress"
# → ROX-4821: transitioned to "In Progress"
jai transition ROX-4821 --list
Expand All @@ -368,9 +372,13 @@ jai open ROX-4821 --url-only # print URL only

# Add a comment
jai comment ROX-4821 "Fixed in PR #4892, deploying to staging"
# → ROX-4821: comment added (pending sync)
# → ROX-4821: comment added ✓

# Use --queue to defer
jai comment ROX-4821 "Will investigate tomorrow" --queue
# → ROX-4821: comment added (queued)

# Push all pending changes
# Push queued changes (only needed after --queue)
jai push
# → ✓ ROX-4821: priority → "High"
# → ✓ ROX-4821: labels updated
Expand Down Expand Up @@ -450,7 +458,7 @@ Both paths can be overridden with `--config` and `--db` flags, or by setting `db
| `jai open <key>` | Open issue in browser (`--url-only` to print URL) |
| `jai clone <key>` | Clone an issue with optional overrides |
| `jai create <project>` | Create a new issue (`--template`, `--body`) |
| `jai set <key> <field> <value>` | Update an issue field |
| `jai set <key> <field> <value>` | Update an issue field (pushes immediately; `--queue` to defer) |
| `jai set <key> <field> --add <val>` | Add a value to an array field |
| `jai set <key> <field> --remove <val>` | Remove a value from an array field |
| `jai set K1,K2,K3 <field> <value>` | Bulk set on comma-separated keys |
Expand All @@ -459,8 +467,8 @@ Both paths can be overridden with `--config` and `--db` flags, or by setting `db
| `jai link <from> <to>` | Link two issues or add a remote URL link |
| `jai watch <key>` | Add yourself (or a user) as watcher |
| `jai unwatch <key>` | Remove yourself as watcher |
| `jai comment <key> <text>` | Add a comment |
| `jai push` | Push pending changes to Jira |
| `jai comment <key> <text>` | Add a comment (pushes immediately; `--queue` to defer) |
| `jai push` | Push queued changes to Jira (only needed after `--queue`) |
| `jai tui` | Launch full-screen TUI |
| `jai completion <shell>` | Generate shell completions (bash/zsh/fish) |

Expand Down
31 changes: 24 additions & 7 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@ This runs during `jai sync --full` and optionally on a configurable schedule.

### Write Queue Processing

By default, `jai set`, `jai comment`, and `jai transition` call the Jira API immediately (write-through). Pass `--queue` / `-q` to defer the change to the `pending_changes` table instead.

When `--queue` is used, changes accumulate in `pending_changes` and are pushed to Jira via `jai push`.

```go
func (w *Writer) ProcessQueue(ctx context.Context) error {
// 1. SELECT from pending_changes WHERE synced_at IS NULL ORDER BY created_at
Expand Down Expand Up @@ -692,27 +696,39 @@ Output: Sync progress and summary

```
Input: issue key, field name, new value (positional)
Output: Confirmation + pending status
Flags: --queue / -q Queue the change instead of writing immediately
Output: Confirmation + status

ROX-123: status → "In Progress" (pending sync)
ROX-123: status → "In Progress" ✓ # default (write-through)
ROX-123: status → "In Progress" (queued) # with --queue

Writes to pending_changes table. If auto_sync is on, immediately attempts to push.
Default behavior calls the Jira API immediately. With --queue, writes to
pending_changes table instead; use `jai push` to sync queued changes.
```

#### `jai comment <key> <text>`

```
Input: issue key, comment text (positional)
Output: Confirmation + pending status
Flags: --queue / -q Queue the comment instead of posting immediately
Output: Confirmation + status

ROX-123: comment added ✓ # default (write-through)
ROX-123: comment added (queued) # with --queue

ROX-123: comment added (pending sync)
Default behavior calls the Jira API immediately. AddComment returns
(string, error) so write-through comments receive real Jira IDs.
With --queue, writes to pending_changes table instead.
```

#### `jai push`

```
Output: Push queue processing summary

Only needed for changes created with --queue. Write-through changes
(the default) are already synced and do not appear in the push queue.

Pushing 3 pending changes...
✓ ROX-123: status → "In Progress"
✓ ROX-123: comment added
Expand Down Expand Up @@ -829,6 +845,8 @@ type SyncMsg struct {

### Pending Change Highlighting

The TUI inline field editor writes through to Jira by default via a background `tea.Cmd`. If the API call fails, the change falls back to the `pending_changes` queue automatically.

Issues with pending (unsynced) changes display a visual indicator:

```
Expand All @@ -837,8 +855,7 @@ Issues with pending (unsynced) changes display a visual indicator:
ROX-456 | Update docs | To Do
```

The `⟳` marker (or color highlight) shows that ROX-123 has a pending status change that hasn't been confirmed by sync yet. Cleared once incremental sync returns the confirmed state.

The `⟳` marker (or color highlight) shows that ROX-123 has a pending status change that hasn't been confirmed by sync yet. This appears when a write-through call fails and the change is queued, or when `--queue` was used explicitly. Cleared once incremental sync returns the confirmed state.
### Color Rules Evaluation

```go
Expand Down
8 changes: 6 additions & 2 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jai schema get # command schema for agents

## Write operations

All write commands (except `transition` and `link`) queue changes locally. Run `jai push` to sync them to Jira.
Write commands push changes to Jira immediately by default. Use `--queue` / `-q` to defer changes locally until you run `jai push`.

### Set a field

Expand Down Expand Up @@ -157,7 +157,7 @@ jai set --query "SELECT key FROM issues WHERE type = 'Bug' AND status = 'To Do'"

### Transitions

Transition an issue to a new workflow status. Transitions push to Jira immediately (no `jai push` needed).
Transition an issue to a new workflow status. Like all write commands, transitions push to Jira immediately. Use `--queue` to defer.

```sh
# List available transitions
Expand Down Expand Up @@ -211,6 +211,8 @@ jai unwatch ROX-123 # remove yourself
jai comment ROX-123 "Fixed in PR #4892"
```

Comments push to Jira immediately. Use `--queue` / `-q` to defer until `jai push`.

### Create issues

```sh
Expand Down Expand Up @@ -246,6 +248,8 @@ jai open ROX-123 --url-only # print URL to stdout

### Push

Push is only needed for changes that were deferred with `--queue`. It syncs all queued changes to Jira.

```sh
jai push
```
Expand Down
Loading
Loading