@tomkapa/tayto 0.3.2 → 0.4.1

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/README.md CHANGED
@@ -1,98 +1,141 @@
1
- # tayto
1
+ <div align="center">
2
2
 
3
- Task management for solo developers and AI agents. Two interfaces, one database.
3
+ ![Tayto - Task management for solo developers and AI agents](banner.png)
4
4
 
5
- - **CLI** returns structured JSON &mdash; built for AI agents and scripting
6
- - **TUI** renders rich markdown in the terminal &mdash; built for humans
5
+ [![npm](https://img.shields.io/npm/v/@tomkapa/tayto)](https://www.npmjs.com/package/@tomkapa/tayto)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D25-brightgreen)](https://nodejs.org)
7
8
 
8
- SQLite-backed. No server. No login. Just projects and tasks.
9
+ **Stop losing tasks between AI sessions. Stop drowning in Jira fields you don't need.**
10
+
11
+ Tayto is a local-first task manager built for solo developers who work with AI coding agents. Two interfaces, one SQLite database, zero configuration.
12
+
13
+ [Quick Start](#quick-start) &bull; [Why Tayto](#why-tayto) &bull; [Workflow](#workflow) &bull; [CLI Reference](#cli-reference) &bull; [TUI Reference](#tui-reference)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## Why Tayto
20
+
21
+ Every project management tool out there assumes you're on a team. They want you to configure sprints, assign story points, set due dates, and fill out fifteen fields before you can track a single task.
22
+
23
+ If you're a solo dev shipping with AI agents like Claude Code, you need something different:
24
+
25
+ - **You forget things.** A quick idea during a coding session, a tech debt note from an AI-generated feature, a bug you noticed but can't fix right now. Without a fast capture tool, these vanish.
26
+ - **AI generates work faster than you can track it.** Your agent builds five features in an afternoon. Each one leaves behind edge cases, missing tests, and shortcuts. That debt is invisible until it bites you.
27
+ - **Priority fields are a lie.** When you're the only one executing, all that matters is order: what's first, what's next. Row 1 in the task list is what you do now. That's it.
28
+ - **Your AI agent can't use Jira.** It needs a CLI that speaks JSON. Your existing tools weren't built for this.
29
+
30
+ Tayto solves exactly this: a **CLI for agents** and a **TUI for humans**, sharing the same local SQLite database. No server. No login. No internet required.
31
+
32
+ ---
33
+
34
+ ## Workflow
35
+
36
+ Tayto follows a lean, agile-inspired loop designed for how solo devs actually work:
37
+
38
+ ![Workflow](workflow.png)
39
+
40
+ **1. Capture** &mdash; Use AI to generate tasks from feature plans, record tech debt, or break down epics. Never lose an idea again.
41
+
42
+ **2. Prioritize** &mdash; You review the backlog and drag tasks into execution order. No story points. No priority matrices. Just: what's first?
43
+
44
+ **3. Enrich** &mdash; AI analyzes the codebase and writes implementation-ready technical notes for the top backlog items.
45
+
46
+ **4. Review** &mdash; You read the plan. Approve it, adjust it, or send it back.
47
+
48
+ **5. Execute** &mdash; AI implements the first `todo` task. You review the code. Cycle repeats.
49
+
50
+ > Don't plan too far ahead. Keep tasks in the backlog. If something becomes urgent, re-rank it. Enrich only what's next. Ship what's ready. Simple.
9
51
 
10
52
  ---
11
53
 
12
54
  ## Quick Start
13
55
 
14
56
  ```bash
15
- # Install
16
- npm install && npm run build
57
+ # Install globally from npm
58
+ npm install -g @tomkapa/tayto
17
59
 
18
- # Create a project
60
+ # Create a project (auto-links to current git repo)
19
61
  tayto project create -n "my-app" --default
20
62
 
21
- # Create tasks
22
- tayto task create -n "Fix auth bug" -t bug --priority 1
23
- tayto task create -n "Add dashboard" -t story --priority 2
63
+ # Capture some tasks
64
+ tayto task create -n "Fix auth token refresh" -t bug
65
+ tayto task create -n "Add user dashboard" -t story
66
+ tayto task create -n "Refactor DB connection pooling" -t tech-debt
67
+
68
+ # Re-rank: put the bug at the top
69
+ tayto task rank <bug-id> --top
24
70
 
25
- # Launch the terminal UI
71
+ # Launch the TUI to review your backlog
26
72
  tayto
27
73
  ```
28
74
 
29
- ## Installation
75
+ ---
30
76
 
31
- **Requirements:** Node.js >= 18
77
+ ## Installation
32
78
 
33
79
  ```bash
34
- git clone <repo-url> && cd tayto
35
- npm install
36
- npm run build
37
- npm link # makes `tayto` available globally
80
+ npm install -g @tomkapa/tayto
38
81
  ```
39
82
 
40
- ## Usage
41
-
42
- ### Terminal UI
43
-
44
- Run `tayto` with no arguments to launch the interactive TUI.
83
+ Or build from source:
45
84
 
46
85
  ```bash
47
- tayto # launch TUI (default)
48
- tayto tui # explicit launch
49
- tayto tui -p "my-app" # start with a specific project
86
+ git clone https://github.com/tomkapa/tayto && cd tayto
87
+ npm install && npm run build
88
+ npm link
50
89
  ```
51
90
 
52
- #### Keyboard Shortcuts
91
+ **Requires:** Node.js >= 25
53
92
 
54
- | Key | Action |
55
- |---|---|
56
- | `j` / `k` / `arrows` | Navigate up/down |
57
- | `Enter` | Open task detail |
58
- | `c` | Create task |
59
- | `e` | Edit task |
60
- | `d` | Delete task (with confirmation) |
61
- | `s` | Cycle status forward |
62
- | `/` | Search tasks |
63
- | `f` | Cycle status filter |
64
- | `t` | Cycle type filter |
65
- | `1`-`5` | Toggle priority filter |
66
- | `0` | Clear all filters |
67
- | `p` | Switch project |
68
- | `Esc` / `b` | Go back |
69
- | `?` | Show help |
70
- | `q` | Quit |
93
+ ---
71
94
 
72
- The task detail view renders **markdown**, **code blocks**, and **technical notes** directly in the terminal.
95
+ ## Two Interfaces, One Database
73
96
 
74
- ### CLI Commands
97
+ ### CLI &mdash; Built for AI Agents and Scripts
75
98
 
76
- All commands output JSON to stdout. Errors go to stderr with exit code 1.
99
+ Every command returns structured JSON to stdout. Errors go to stderr with exit code 1.
77
100
 
78
101
  ```jsonc
79
- // success
102
+ // Success
80
103
  { "ok": true, "data": { ... } }
81
104
 
82
- // error
105
+ // Error
83
106
  { "ok": false, "error": { "code": "NOT_FOUND", "message": "..." } }
84
107
  ```
85
108
 
86
- #### Project
109
+ This makes Tayto a natural fit for AI coding agents. Claude Code can create tasks, search the backlog, read technical notes, and update status &mdash; all through simple CLI calls.
110
+
111
+ ### TUI &mdash; Built for Humans
112
+
113
+ Run `tayto` with no arguments to launch an interactive terminal UI with:
114
+
115
+ - Rich markdown rendering for task descriptions and technical notes
116
+ - Vim-style navigation (`j`/`k`)
117
+ - Inline filtering by status, type, and priority
118
+ - Full-text search across all task fields
119
+ - Task creation, editing, and status management without leaving the terminal
120
+
121
+ ---
122
+
123
+ ## CLI Reference
124
+
125
+ ### Project Management
87
126
 
88
127
  ```bash
89
128
  tayto project create -n "my-app" -d "Description" --default
90
129
  tayto project list
91
130
  tayto project update <id> -n "new-name" --default
92
131
  tayto project delete <id>
132
+ tayto project link <id> --remote <git-remote-url>
133
+ tayto project unlink <id>
93
134
  ```
94
135
 
95
- #### Task
136
+ Projects auto-detect the current git remote, so `tayto` in a repo directory uses the right project automatically.
137
+
138
+ ### Task Management
96
139
 
97
140
  ```bash
98
141
  # Create
@@ -100,115 +143,151 @@ tayto task create \
100
143
  -n "Fix login bug" \
101
144
  -t bug \
102
145
  -s todo \
103
- --priority 1 \
104
146
  -p "my-app" \
105
147
  -d "Login fails on mobile" \
106
- --technical-notes "Check JWT expiry" \
148
+ --technical-notes "Check JWT expiry logic" \
107
149
  --additional-requirements "Must work on iOS Safari"
108
150
 
109
- # Read
151
+ # List and search
110
152
  tayto task list
111
- tayto task list --status in-progress --type bug --search "login"
112
- tayto task list --priority 1 --parent <parent-id>
113
- tayto task show <id>
153
+ tayto task list --status in-progress --type bug
154
+ tayto task search "login"
114
155
 
115
156
  # Update
116
157
  tayto task update <id> -s in-progress
117
158
  tayto task update <id> --append-notes "Root cause: token not refreshed"
118
159
  tayto task update <id> --append-requirements "Also fix on Android"
119
160
 
161
+ # Re-rank (execution order)
162
+ tayto task rank <id> --top
163
+ tayto task rank <id> --bottom
164
+ tayto task rank <id> --before <other-id>
165
+ tayto task rank <id> --after <other-id>
166
+ tayto task rank <id> --position 3
167
+
168
+ # Break down into subtasks
169
+ tayto task breakdown <parent-id> -f subtasks.json
170
+
120
171
  # Delete
121
172
  tayto task delete <id>
122
173
 
123
- # Breakdown (create subtasks from JSON)
124
- tayto task breakdown <parent-id> -f subtasks.json
174
+ # Export / Import
175
+ tayto task export -o backup.json
176
+ tayto task import -f backup.json
125
177
  ```
126
178
 
127
- **subtasks.json** example:
179
+ ### Dependency Management
128
180
 
129
- ```json
130
- [
131
- { "name": "Implement API endpoint", "type": "story", "priority": 2 },
132
- { "name": "Write integration tests", "type": "story", "priority": 3 }
133
- ]
181
+ ```bash
182
+ tayto dep add <task-id> <depends-on-id>
183
+ tayto dep add <task-id> <depends-on-id> -t blocked-by
184
+ tayto dep remove <task-id> <depends-on-id>
185
+ tayto dep list <task-id>
186
+ tayto dep graph <task-id>
134
187
  ```
135
188
 
189
+ Dependency types: `blocks`, `blocked-by`, `relates-to`, `duplicates`
190
+
191
+ ---
192
+
193
+ ## TUI Reference
194
+
195
+ Launch with `tayto` or `tayto tui`.
196
+
197
+ | Key | Action |
198
+ |---|---|
199
+ | `j` / `k` / arrows | Navigate |
200
+ | `Enter` | Open task detail |
201
+ | `c` | Create task |
202
+ | `e` | Edit task |
203
+ | `d` | Delete task |
204
+ | `s` | Cycle status |
205
+ | `/` | Search |
206
+ | `f` | Filter by status |
207
+ | `t` | Filter by type |
208
+ | `0` | Clear filters |
209
+ | `p` | Switch project |
210
+ | `?` | Help |
211
+ | `q` | Quit |
212
+
213
+ ---
214
+
136
215
  ## Data Model
137
216
 
138
217
  ### Task Types
139
218
 
140
- | Type | Description |
219
+ | Type | Use for |
141
220
  |---|---|
142
- | `story` | Feature or user story |
143
- | `tech-debt` | Refactoring or cleanup |
144
- | `bug` | Defect or issue |
221
+ | `story` | Features and user stories |
222
+ | `tech-debt` | Refactoring, cleanup, missing tests |
223
+ | `bug` | Defects and issues |
145
224
 
146
- ### Task Statuses
225
+ ### Statuses
147
226
 
148
- `backlog` &rarr; `todo` &rarr; `in-progress` &rarr; `review` &rarr; `done`
227
+ `backlog` &rarr; `todo` &rarr; `in-progress` &rarr; `review` &rarr; `done` (or `cancelled`)
149
228
 
150
- `cancelled` is also available for abandoned tasks.
229
+ ### Task Fields
151
230
 
152
- ### Priority
231
+ Each task carries rich context for AI consumption:
153
232
 
154
- | Level | Label |
155
- |---|---|
156
- | 1 | Critical |
157
- | 2 | High |
158
- | 3 | Medium (default) |
159
- | 4 | Low |
160
- | 5 | Lowest |
233
+ - **name** &mdash; short summary
234
+ - **description** &mdash; user-facing details
235
+ - **technical_notes** &mdash; implementation guidance (appendable)
236
+ - **additional_requirements** &mdash; constraints and edge cases (appendable)
237
+ - **rank** &mdash; execution order within the project
238
+ - **parent_id** &mdash; hierarchical breakdown support
161
239
 
162
- ### Task Breakdown
163
-
164
- Tasks support a `parent_id` field for hierarchical decomposition. Use `task breakdown` to batch-create subtasks under a parent, or pass `--parent <id>` on `task create`.
240
+ ---
165
241
 
166
242
  ## Configuration
167
243
 
168
- Configure via environment variables.
169
-
170
244
  | Variable | Default | Description |
171
245
  |---|---|---|
172
- | `TASK_DB_PATH` | `~/.task/data.db` | Path to SQLite database |
246
+ | `TASK_DB_PATH` | `~/.task/data.db` | SQLite database path |
173
247
  | `TASK_DATA_DIR` | `~/.task` | Data directory |
174
- | `TASK_LOG_LEVEL` | `info` | Log level (`debug`, `info`, `warn`, `error`) |
175
- | `OTEL_EXPORTER_OTLP_ENDPOINT` | &mdash; | OpenTelemetry collector endpoint |
248
+ | `TASK_LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` |
249
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | &mdash; | OpenTelemetry collector |
250
+
251
+ Database is created automatically on first run. All data stays on your machine.
252
+
253
+ ---
254
+
255
+ ## Claude Code Integration
176
256
 
177
- The database and data directory are created automatically on first run.
257
+ Tayto ships with Claude Code skills for the full AI-assisted workflow:
258
+
259
+ | Skill | What it does |
260
+ |---|---|
261
+ | `/tayto` | Manage projects and tasks from conversation |
262
+ | `/implement-task` | Pick the top todo and implement it |
263
+ | `/enrich-task` | Research the codebase and write technical notes for the next backlog item |
264
+
265
+ The CLI's JSON output format means any AI agent with shell access can interact with Tayto &mdash; no special integration needed.
266
+
267
+ ---
178
268
 
179
269
  ## Architecture
180
270
 
181
271
  ```
182
- CLI Commands ──┐
183
- ├──> Service Layer ──> Repository Layer ──> SQLite
184
- Terminal UI ───┘
272
+ CLI (Commander.js) ──┐
273
+ ├──> Service Layer (Zod validation) ──> Repository ──> SQLite
274
+ TUI (Ink/React) ─────┘
185
275
  ```
186
276
 
187
- - **Service layer** handles validation (Zod), business logic, and project resolution
188
- - **Repository layer** handles SQL queries with parameterized statements
189
- - **Result\<T\>** return type across all layers &mdash; no thrown exceptions for business logic
190
- - **OpenTelemetry** spans on every service and repository operation
191
- - **ULID** identifiers &mdash; sortable, no database round-trip
277
+ - **Result\<T\>** return type across all layers &mdash; no thrown exceptions
278
+ - **OpenTelemetry** tracing on every operation
279
+ - **FTS5** full-text search across task fields
280
+ - **ULID** identifiers &mdash; sortable, collision-free
281
+ - **Fractional ranking** &mdash; O(1) reorder without renumbering
192
282
 
193
- ```
194
- src/
195
- cli/ # Commander.js commands, JSON output
196
- tui/ # Ink (React) terminal UI components
197
- service/ # Business logic
198
- repository/ # Data access
199
- db/ # SQLite connection, migrations
200
- types/ # Zod schemas, enums, Result type
201
- errors/ # Typed error hierarchy
202
- logging/ # OpenTelemetry tracer
203
- config/ # Environment-based configuration
204
- ```
283
+ ---
205
284
 
206
285
  ## Development
207
286
 
208
287
  ```bash
209
- npm run dev # build in watch mode
210
- npm run check # prettier + eslint
288
+ npm run dev # watch mode
211
289
  npm run test # run tests
290
+ npm run check # prettier + eslint
212
291
  npm run build # production build
213
292
  ```
214
293
 
@@ -143,8 +143,46 @@ function isTerminalStatus(status) {
143
143
  return TERMINAL_STATUSES.has(status);
144
144
  }
145
145
 
146
+ // src/utils/git.ts
147
+ import { spawnSync } from "child_process";
148
+
149
+ // src/types/common.ts
150
+ function ok(value) {
151
+ return { ok: true, value };
152
+ }
153
+ function err(error) {
154
+ return { ok: false, error };
155
+ }
156
+
157
+ // src/utils/git.ts
158
+ function detectGitRemote(cwd) {
159
+ try {
160
+ const result = spawnSync("git", ["remote", "get-url", "origin"], {
161
+ cwd: cwd ?? process.cwd(),
162
+ encoding: "utf-8",
163
+ timeout: 5e3
164
+ });
165
+ if (result.status !== 0 || result.error) {
166
+ logger.info("detectGitRemote: no git remote found (non-zero exit or error)");
167
+ return ok(null);
168
+ }
169
+ const remote = result.stdout.trim();
170
+ if (!remote) {
171
+ logger.info("detectGitRemote: empty stdout from git remote get-url");
172
+ return ok(null);
173
+ }
174
+ logger.info(`detectGitRemote: found remote=${remote}`);
175
+ return ok(remote);
176
+ } catch (e) {
177
+ logger.info(`detectGitRemote: exception during git detection: ${String(e)}`);
178
+ return ok(null);
179
+ }
180
+ }
181
+
146
182
  export {
147
183
  logger,
184
+ ok,
185
+ err,
148
186
  TaskStatus,
149
187
  TaskType,
150
188
  TaskLevel,
@@ -155,6 +193,7 @@ export {
155
193
  RANK_GAP,
156
194
  midpoint,
157
195
  TERMINAL_STATUSES,
158
- isTerminalStatus
196
+ isTerminalStatus,
197
+ detectGitRemote
159
198
  };
160
- //# sourceMappingURL=chunk-STYT4TGJ.js.map
199
+ //# sourceMappingURL=chunk-74Q55TOV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logging/logger.ts","../src/types/enums.ts","../src/utils/git.ts","../src/types/common.ts"],"sourcesContent":["import { appendFileSync, readdirSync, unlinkSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { trace, type Span, SpanStatusCode } from '@opentelemetry/api';\n\nconst tracer = trace.getTracer('task');\nconst LOG_RETENTION_DAYS = 7;\n\nexport interface LogAttributes {\n [key: string]: string | number | boolean;\n}\n\ntype LogLevel = 'INFO' | 'WARN' | 'ERROR';\n\nfunction formatTimestamp(): string {\n return new Date().toISOString();\n}\n\nfunction formatAttrs(attrs?: LogAttributes): string {\n if (!attrs || Object.keys(attrs).length === 0) return '';\n return ' ' + JSON.stringify(attrs);\n}\n\nclass Logger {\n private logFilePath: string | null = null;\n\n init(logDir: string): void {\n const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD\n this.logFilePath = join(logDir, `task-${date}.log`);\n this.pruneOldLogs(logDir);\n }\n\n info(message: string, attrs?: LogAttributes): void {\n this.write('INFO', message, attrs);\n const span = trace.getActiveSpan();\n if (span) {\n span.addEvent(message, attrs);\n }\n }\n\n warn(message: string, attrs?: LogAttributes): void {\n this.write('WARN', message, attrs);\n const span = trace.getActiveSpan();\n if (span) {\n span.addEvent(`WARN: ${message}`, attrs);\n }\n }\n\n error(message: string, error?: unknown, attrs?: LogAttributes): void {\n const errorDetail = error instanceof Error ? ` | ${error.stack ?? error.message}` : '';\n this.write('ERROR', `${message}${errorDetail}`, attrs);\n const span = trace.getActiveSpan();\n if (span) {\n span.addEvent(`ERROR: ${message}`, attrs);\n if (error instanceof Error) {\n span.recordException(error);\n }\n span.setStatus({ code: SpanStatusCode.ERROR, message });\n }\n }\n\n startSpan<T>(name: string, fn: (span: Span) => T): T {\n return tracer.startActiveSpan(name, (span) => {\n try {\n const result = fn(span);\n span.end();\n return result;\n } catch (e) {\n if (e instanceof Error) {\n span.recordException(e);\n }\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.end();\n throw e;\n }\n });\n }\n\n private write(level: LogLevel, message: string, attrs?: LogAttributes): void {\n if (!this.logFilePath) return;\n const line = `${formatTimestamp()} [${level}] ${message}${formatAttrs(attrs)}\\n`;\n try {\n appendFileSync(this.logFilePath, line);\n } catch {\n // Swallowing here is intentional: logging must never crash the app.\n // If the log file is unwritable, the OTel span still captures the event.\n }\n }\n\n private pruneOldLogs(logDir: string): void {\n try {\n const cutoff = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n const files = readdirSync(logDir).filter((f) => f.startsWith('task-') && f.endsWith('.log'));\n for (const file of files) {\n const dateStr = file.slice('task-'.length, -'.log'.length);\n const fileDate = new Date(dateStr).getTime();\n if (!isNaN(fileDate) && fileDate < cutoff) {\n unlinkSync(join(logDir, file));\n }\n }\n } catch {\n // Best-effort cleanup — don't crash if pruning fails\n }\n }\n}\n\nexport const logger = new Logger();\n","export const TaskStatus = {\n Backlog: 'backlog',\n Todo: 'todo',\n InProgress: 'in-progress',\n Review: 'review',\n Done: 'done',\n Cancelled: 'cancelled',\n} as const;\nexport type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];\n\nexport const TaskType = {\n Epic: 'epic',\n Story: 'story',\n TechDebt: 'tech-debt',\n Bug: 'bug',\n} as const;\nexport type TaskType = (typeof TaskType)[keyof typeof TaskType];\n\n/**\n * Task level derived from type.\n * Level 1: epics (grouping/planning layer)\n * Level 2: stories, tech-debt, bugs (execution layer)\n */\nexport const TaskLevel = {\n Epic: 1,\n Work: 2,\n} as const;\nexport type TaskLevel = (typeof TaskLevel)[keyof typeof TaskLevel];\n\nconst TYPE_TO_LEVEL: Record<string, TaskLevel> = {\n [TaskType.Epic]: TaskLevel.Epic,\n [TaskType.Story]: TaskLevel.Work,\n [TaskType.TechDebt]: TaskLevel.Work,\n [TaskType.Bug]: TaskLevel.Work,\n};\n\nexport function getTaskLevel(type: string): TaskLevel {\n return TYPE_TO_LEVEL[type] ?? TaskLevel.Work;\n}\n\n/** Types that belong to the work (level 2) execution layer. */\nexport const WORK_TYPES: ReadonlySet<string> = new Set([\n TaskType.Story,\n TaskType.TechDebt,\n TaskType.Bug,\n]);\n\n/** Types stored in the database. */\nexport const DependencyType = {\n Blocks: 'blocks',\n RelatesTo: 'relates-to',\n Duplicates: 'duplicates',\n} as const;\nexport type DependencyType = (typeof DependencyType)[keyof typeof DependencyType];\n\n/**\n * UI-level dependency types — includes BlockedBy which is a reverse-Blocks\n * relationship resolved before persisting to the database.\n */\nexport const UIDependencyType = {\n ...DependencyType,\n BlockedBy: 'blocked-by',\n} as const;\nexport type UIDependencyType = (typeof UIDependencyType)[keyof typeof UIDependencyType];\n\n/** Gap between consecutive rank values, used for insertion between neighbors. */\nexport const RANK_GAP = 1000.0;\n\n/**\n * Collapse-safe midpoint between two rank values. Returns `null` when\n * IEEE 754 double precision cannot represent a strictly-between value —\n * callers use this as a signal to rebalance the rank grid and retry.\n * Returning a number unconditionally would silently collide with an\n * endpoint and corrupt the task ordering.\n */\nexport function midpoint(a: number, b: number): number | null {\n const m = (a + b) / 2;\n return m > a && m < b ? m : null;\n}\n\n/** Statuses that represent terminal/completed task states. */\nexport const TERMINAL_STATUSES: ReadonlySet<string> = new Set([\n TaskStatus.Done,\n TaskStatus.Cancelled,\n]);\n\nexport function isTerminalStatus(status: string): boolean {\n return TERMINAL_STATUSES.has(status);\n}\n","import { spawnSync } from 'node:child_process';\nimport type { Result } from '../types/common.js';\nimport { ok } from '../types/common.js';\nimport { logger } from '../logging/logger.js';\n\n/**\n * Detect the git origin remote URL from the given directory.\n * Returns ok(url) if found, ok(null) if no git repo or no origin remote.\n * Never returns err() — all git failures are silent fallbacks.\n */\nexport function detectGitRemote(cwd?: string): Result<string | null> {\n try {\n const result = spawnSync('git', ['remote', 'get-url', 'origin'], {\n cwd: cwd ?? process.cwd(),\n encoding: 'utf-8',\n timeout: 5000,\n });\n\n if (result.status !== 0 || result.error) {\n logger.info('detectGitRemote: no git remote found (non-zero exit or error)');\n return ok(null);\n }\n\n const remote = result.stdout.trim();\n if (!remote) {\n logger.info('detectGitRemote: empty stdout from git remote get-url');\n return ok(null);\n }\n\n logger.info(`detectGitRemote: found remote=${remote}`);\n return ok(remote);\n } catch (e: unknown) {\n logger.info(`detectGitRemote: exception during git detection: ${String(e)}`);\n return ok(null);\n }\n}\n","import type { AppError } from '../errors/app-error.js';\n\nexport type Result<T, E = AppError> = { ok: true; value: T } | { ok: false; error: E };\n\nexport function ok<T>(value: T): Result<T, never> {\n return { ok: true, value };\n}\n\nexport function err<E>(error: E): Result<never, E> {\n return { ok: false, error };\n}\n\nexport interface CLIOutput<T> {\n ok: boolean;\n data?: T;\n error?: { code: string; message: string };\n}\n"],"mappings":"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","names":[]}