mirror of
https://github.com/bitsofchris/openaugi-obsidian-plugin.git
synced 2026-07-22 12:40:27 +00:00
Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc62c4d591 | ||
|
|
09892d2ef3 | ||
|
|
cac28bd98d | ||
|
|
6ff04f0f33 | ||
|
|
cbc0c1b471 | ||
|
|
521399efd3 | ||
|
|
6e9ed65d40 | ||
|
|
117b33eb95 |
19 changed files with 1031 additions and 220 deletions
19
CLAUDE.md
19
CLAUDE.md
|
|
@ -138,16 +138,19 @@ Tests cover: filename utils, OpenAI prompt building, link extraction, BFS traver
|
||||||
- Enable verbose logging in development
|
- Enable verbose logging in development
|
||||||
|
|
||||||
### Publishing
|
### Publishing
|
||||||
See [docs/PUBLISHING.md](docs/PUBLISHING.md) for the complete release process.
|
See [docs/PUBLISHING.md](docs/PUBLISHING.md) for the complete release process,
|
||||||
|
including community-store listing health and how to relist if de-listed.
|
||||||
|
|
||||||
**Quick summary:**
|
**Just run the script** (it bumps all three version files, publishes, and verifies):
|
||||||
1. Update version in `manifest.json` and `package.json`
|
```bash
|
||||||
2. Commit and push to master
|
# write docs/release-notes/X.Y.Z.md first, then:
|
||||||
3. Tag with matching version: `git tag -a X.Y.Z -m "X.Y.Z" && git push origin X.Y.Z`
|
./scripts/release.sh X.Y.Z
|
||||||
4. Generate release notes (Claude: compare commits since last tag, focus on user-facing changes)
|
```
|
||||||
5. Edit draft release on GitHub and publish
|
|
||||||
|
|
||||||
*** Be sure to update `manifest.json` version number as part of PR ***
|
**Non-negotiables** (the guard test `tests/version-consistency.test.ts` enforces these):
|
||||||
|
- Bump **all THREE** version files together: `manifest.json`, `package.json`, **and `versions.json`** (add `"X.Y.Z": "<minAppVersion>"`). Forgetting `versions.json` is the classic mistake.
|
||||||
|
- Tag == `manifest.version`, no `v` prefix.
|
||||||
|
- **Publish the draft immediately** after CI — never leave the repo advertising a version with no published release (that inconsistency can get the plugin auto-removed from the store).
|
||||||
|
|
||||||
## Important Considerations
|
## Important Considerations
|
||||||
|
|
||||||
|
|
|
||||||
76
README.md
76
README.md
|
|
@ -23,15 +23,18 @@ Gather precisely the right context from your vault — not too much, not too lit
|
||||||
- **Character budgets** — Stay within token limits with configurable caps
|
- **Character budgets** — Stay within token limits with configurable caps
|
||||||
- **Checkbox review** — Toggle individual notes on/off before processing
|
- **Checkbox review** — Toggle individual notes on/off before processing
|
||||||
|
|
||||||
### 2. Task Dispatch
|
### 2. Agent Tasks
|
||||||
|
|
||||||
Write a task note, link your context, and launch an agent session directly from Obsidian.
|
Queue work for your agents without leaving Obsidian. The **Augi** commands write task files to `OpenAugi/Tasks/` — the [OpenAugi task watcher](https://github.com/bitsofchris/openaugi) picks them up and runs an agent session for each.
|
||||||
|
|
||||||
- **tmux sessions** — Each task gets its own persistent terminal session
|
- **Augi: Run review pass** — Route new blocks, refresh views, surface Dashboard nominations
|
||||||
- **Context injection** — Task note body + all linked notes are assembled and passed to the agent
|
- **Augi: Process dashboard** — Execute your nomination answers only
|
||||||
- **Named repo paths** — Map short names to directories so `working_dir: my-api` just works
|
- **Augi: Distill selection** — Distill the current selection (or active note) through the distill lens
|
||||||
- **Session management** — List, attach to, or kill running agent sessions
|
- **File-based trigger** — Pure vault API, no shell or HTTP, works on Obsidian Mobile with a synced vault
|
||||||
- **MCP integration** — Agents can search your vault and write results back to the task note
|
|
||||||
|
See [Agent Tasks docs](docs/AGENT_TASKS.md).
|
||||||
|
|
||||||
|
The older **Task Dispatch** feature (launching tmux sessions directly from the plugin) is deprecated in favor of the task-file flow — see [Task Dispatch docs](docs/TASK_DISPATCH.md).
|
||||||
|
|
||||||
### 3. Note Processing
|
### 3. Note Processing
|
||||||
|
|
||||||
|
|
@ -50,30 +53,13 @@ Turn raw notes into organized, atomic knowledge.
|
||||||
|
|
||||||
1. Install from Obsidian Community Plugins (or manually)
|
1. Install from Obsidian Community Plugins (or manually)
|
||||||
2. Settings → OpenAugi → Enter your OpenAI API key
|
2. Settings → OpenAugi → Enter your OpenAI API key
|
||||||
3. For task dispatch: install tmux (`brew install tmux`)
|
3. For agent tasks: install [OpenAugi](https://github.com/bitsofchris/openaugi) and run `openaugi up` (the task watcher)
|
||||||
|
|
||||||
### Dispatch a Task
|
### Queue an Agent Task
|
||||||
|
|
||||||
Create a task note:
|
Run **Augi: Run review pass** (or **Augi: Process dashboard**, **Augi: Distill selection**) from the command palette. The plugin writes a pending task file to `OpenAugi/Tasks/`; the task watcher launches an agent session that does the work and writes its results back into the task file.
|
||||||
|
|
||||||
```yaml
|
For **Augi: Distill selection**, select the text you want distilled first — the selection (or the whole active note if nothing is selected) becomes the task's context.
|
||||||
---
|
|
||||||
task_id: fix-auth-bug
|
|
||||||
working_dir: my-repo
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Objective
|
|
||||||
|
|
||||||
Fix the authentication bug where sessions expire after 5 minutes.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The auth middleware is in `src/middleware/auth.ts`. [[API Design Doc]] has the spec.
|
|
||||||
```
|
|
||||||
|
|
||||||
Run **Task dispatch: Launch or attach** from the command palette. A terminal opens with your agent pre-loaded with context from the note and all linked notes.
|
|
||||||
|
|
||||||
### Gather Context
|
### Gather Context
|
||||||
|
|
||||||
|
|
@ -89,29 +75,34 @@ Run **Task dispatch: Launch or attach** from the command palette. A terminal ope
|
||||||
|
|
||||||
| Command | Purpose |
|
| Command | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
|
| **Augi: Run review pass** | Queue a full review pass for the task watcher |
|
||||||
|
| **Augi: Process dashboard** | Queue Dashboard nomination processing for the task watcher |
|
||||||
|
| **Augi: Distill selection** | Queue a distill of the current selection or active note |
|
||||||
| **Process notes** | Gather linked notes → review → distill / publish / save |
|
| **Process notes** | Gather linked notes → review → distill / publish / save |
|
||||||
| **Process recent activity** | Same flow but discovers by recent modification date |
|
| **Process recent activity** | Same flow but discovers by recent modification date |
|
||||||
| **Save context** | Gather and save raw context (no AI processing) |
|
| **Save context** | Gather and save raw context (no AI processing) |
|
||||||
| **Task dispatch: Launch or attach** | Launch agent session from task note |
|
| **Task dispatch: Launch or attach** | Deprecated — launch agent session from task note |
|
||||||
| **Task dispatch: Kill session** | Kill tmux session for current task note |
|
| **Task dispatch: Kill session** | Deprecated — kill tmux session for current task note |
|
||||||
| **Task dispatch: List active sessions** | View and manage all running agent sessions |
|
| **Task dispatch: List active sessions** | Deprecated — view and manage running agent sessions |
|
||||||
| **Parse transcript** | Process voice transcript into atomic notes |
|
| **Parse transcript** | Process voice transcript into atomic notes |
|
||||||
| **Distill linked notes** | Legacy command — use Process notes instead |
|
| **Distill linked notes** | Legacy command — use Process notes instead |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task Dispatch
|
## Agent Tasks
|
||||||
|
|
||||||
Task dispatch is the agent harness. It reads a task note, assembles context from linked notes, creates a tmux session, and launches your agent CLI with everything pre-loaded.
|
The Augi commands are the trigger surface for the OpenAugi agent loop. Each command writes a `status: pending` task file to `OpenAugi/Tasks/`; the task watcher (`openaugi up`) hydrates it, launches a Claude agent session, and the agent writes its results back into the same file.
|
||||||
|
|
||||||
See [Task Dispatch docs](docs/TASK_DISPATCH.md) for the full reference.
|
See [Agent Tasks docs](docs/AGENT_TASKS.md) for the full reference, including the task-file format.
|
||||||
|
|
||||||
**Key concepts:**
|
**Key concepts:**
|
||||||
- `task_id` in frontmatter identifies the task (required)
|
- The vault filesystem is the API — the plugin only writes a file, so the commands work on mobile with a synced vault
|
||||||
- `working_dir` sets where the agent runs — supports named repos, absolute paths, or vault-relative paths
|
- The same contract is shared with the `openaugi review` CLI and the `zzz:` capture grammar
|
||||||
- Linked notes (`[[Design Doc]]`, `[[API Spec]]`) are automatically included in context
|
- The task file doubles as the record: results and status land back in it
|
||||||
- Sessions persist across Obsidian restarts — use "List active sessions" to reattach
|
|
||||||
- Agents can use the OpenAugi MCP to search your vault and write results back
|
### Task Dispatch (deprecated)
|
||||||
|
|
||||||
|
The older agent harness: reads a task note, assembles context from linked notes, creates a tmux session, and launches your agent CLI directly from the plugin. Deprecated in favor of the task-file flow above; it keeps working for now but will be removed over a release or two. See [Task Dispatch docs](docs/TASK_DISPATCH.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -146,7 +137,7 @@ Settings are in **Settings → OpenAugi**.
|
||||||
- Include backlinks (default: on)
|
- Include backlinks (default: on)
|
||||||
- Journal section filtering
|
- Journal section filtering
|
||||||
|
|
||||||
**Task Dispatch:**
|
**Task Dispatch (deprecated):**
|
||||||
- Terminal app (iTerm2 or Terminal.app)
|
- Terminal app (iTerm2 or Terminal.app)
|
||||||
- tmux path (auto-detected or manual)
|
- tmux path (auto-detected or manual)
|
||||||
- Default working directory
|
- Default working directory
|
||||||
|
|
@ -164,8 +155,9 @@ Settings are in **Settings → OpenAugi**.
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- **OpenAI API key** — Required for AI processing (distill, publish, parse)
|
- **OpenAI API key** — Required for AI processing (distill, publish, parse)
|
||||||
- **tmux** — Required for task dispatch (`brew install tmux`)
|
- **OpenAugi task watcher** — Required for the Augi agent-task commands ([openaugi](https://github.com/bitsofchris/openaugi), run `openaugi up`)
|
||||||
- **macOS** — Task dispatch terminal opening uses AppleScript (iTerm2 or Terminal.app)
|
- **tmux** — Required for deprecated task dispatch (`brew install tmux`)
|
||||||
|
- **macOS** — Deprecated task dispatch terminal opening uses AppleScript (iTerm2 or Terminal.app)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
94
docs/AGENT_TASKS.md
Normal file
94
docs/AGENT_TASKS.md
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
---
|
||||||
|
name: Agent Tasks (Augi commands)
|
||||||
|
description: Plugin commands that queue work for the OpenAugi task watcher by writing pending task files to OpenAugi/Tasks/ via the vault API.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent Tasks (Augi commands)
|
||||||
|
|
||||||
|
**When to use:** You want an agent to run the review pass, process the
|
||||||
|
Dashboard, or distill a chunk of your writing — triggered from inside
|
||||||
|
Obsidian, on desktop or mobile.
|
||||||
|
|
||||||
|
**How it works:** The vault filesystem is the API. Each command writes a
|
||||||
|
markdown task file with `status: pending` frontmatter into
|
||||||
|
`OpenAugi/Tasks/`. The [OpenAugi task watcher](https://github.com/bitsofchris/openaugi)
|
||||||
|
(`openaugi up`) polls that folder and launches a Claude agent session for
|
||||||
|
each pending task. The plugin never shells out and never makes an HTTP
|
||||||
|
call — it only writes a file, so the same commands work on Obsidian
|
||||||
|
Mobile against a synced vault.
|
||||||
|
|
||||||
|
The `openaugi review` CLI, the `zzz:` capture grammar, and these commands
|
||||||
|
all converge on the same task-file contract, defined authoritatively in
|
||||||
|
the parent repo at `src/openaugi/templates/task-template.md`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- The [OpenAugi](https://github.com/bitsofchris/openaugi) Python package
|
||||||
|
running its task watcher (`openaugi up`) against your vault. Without it,
|
||||||
|
task files sit in `OpenAugi/Tasks/` as pending until a watcher picks
|
||||||
|
them up.
|
||||||
|
- Agent skill files in your vault under `OpenAugi/AGENT/` (created by
|
||||||
|
`openaugi init`): `review-pass.md` for the review commands,
|
||||||
|
`distill-lens.md` for distill.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Instruction queued | Scope |
|
||||||
|
|---------|--------------------|-------|
|
||||||
|
| **Augi: Run review pass** | `run the review pass` | Full loop: route new blocks → refresh views → Dashboard nominations |
|
||||||
|
| **Augi: Process dashboard** | `process the dashboard` | Execute Dashboard nomination answers only, no new-block routing |
|
||||||
|
| **Augi: Distill selection** | `distill this per OpenAugi/AGENT/distill-lens.md` | Current selection, or the active note's body when nothing is selected |
|
||||||
|
|
||||||
|
For **Distill selection**, the selected text (or note body, frontmatter
|
||||||
|
stripped) is copied verbatim into the task file's `## Context` section —
|
||||||
|
the plugin acts purely as a scope selector; the agent distills exactly
|
||||||
|
that content and nothing else.
|
||||||
|
|
||||||
|
## The task file
|
||||||
|
|
||||||
|
Example produced by **Augi: Run review pass**:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
status: pending
|
||||||
|
source_block_id: obsidian-plugin
|
||||||
|
source_note: "[[OpenAugi plugin]]"
|
||||||
|
---
|
||||||
|
|
||||||
|
# run the review pass
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Triggered via the OpenAugi plugin command "Augi: Run review pass" at 20260707-143000.
|
||||||
|
|
||||||
|
## User instruction
|
||||||
|
|
||||||
|
> run the review pass
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Read OpenAugi/AGENT/review-pass.md and execute: run the review pass.
|
||||||
|
|
||||||
|
## Human Todo
|
||||||
|
|
||||||
|
## Results
|
||||||
|
```
|
||||||
|
|
||||||
|
The watcher hydrates the file (adds `task_id`, `created`,
|
||||||
|
`tmux_session`, flips `status: active`, renames to `TASK-*.md`) and
|
||||||
|
launches the agent. When the agent finishes it fills in `## Results` and
|
||||||
|
sets `status: done` — the task file doubles as the record of what
|
||||||
|
happened.
|
||||||
|
|
||||||
|
No `repo`/`working_dir` key is written, so the watcher defaults the
|
||||||
|
agent's working directory to the vault — correct for review and distill
|
||||||
|
work.
|
||||||
|
|
||||||
|
## Relationship to Task Dispatch (deprecated)
|
||||||
|
|
||||||
|
The older [Task Dispatch](TASK_DISPATCH.md) feature launches tmux
|
||||||
|
sessions directly from the plugin. That is a parallel execution path
|
||||||
|
that drifts from the watcher (different session names, duplicate repo
|
||||||
|
settings) and requires desktop + tmux + macOS. It is deprecated and will
|
||||||
|
be removed over a release or two; use the Augi commands with the task
|
||||||
|
watcher instead.
|
||||||
|
|
@ -13,7 +13,8 @@ A comprehensive reference for navigating and extending this Obsidian plugin.
|
||||||
| Link traversal & aggregation | [services/distill-service.ts](../src/services/distill-service.ts) |
|
| Link traversal & aggregation | [services/distill-service.ts](../src/services/distill-service.ts) |
|
||||||
| Unified context discovery | [services/context-gathering-service.ts](../src/services/context-gathering-service.ts) |
|
| Unified context discovery | [services/context-gathering-service.ts](../src/services/context-gathering-service.ts) |
|
||||||
| Settings UI | [ui/settings-tab.ts](../src/ui/settings-tab.ts) |
|
| Settings UI | [ui/settings-tab.ts](../src/ui/settings-tab.ts) |
|
||||||
| Task dispatch service | [services/task-dispatch-service.ts](../src/services/task-dispatch-service.ts) |
|
| Agent task-file writer | [services/task-file-service.ts](../src/services/task-file-service.ts) |
|
||||||
|
| Task dispatch service (deprecated) | [services/task-dispatch-service.ts](../src/services/task-dispatch-service.ts) |
|
||||||
| Task dispatch types | [types/task-dispatch.ts](../src/types/task-dispatch.ts) |
|
| Task dispatch types | [types/task-dispatch.ts](../src/types/task-dispatch.ts) |
|
||||||
| Context modals | [ui/context-gathering-modal.ts](../src/ui/context-gathering-modal.ts), [ui/context-selection-modal.ts](../src/ui/context-selection-modal.ts), [ui/context-preview-modal.ts](../src/ui/context-preview-modal.ts) |
|
| Context modals | [ui/context-gathering-modal.ts](../src/ui/context-gathering-modal.ts), [ui/context-selection-modal.ts](../src/ui/context-selection-modal.ts), [ui/context-preview-modal.ts](../src/ui/context-preview-modal.ts) |
|
||||||
| Session list modal | [ui/session-list-modal.ts](../src/ui/session-list-modal.ts) |
|
| Session list modal | [ui/session-list-modal.ts](../src/ui/session-list-modal.ts) |
|
||||||
|
|
@ -35,7 +36,8 @@ src/
|
||||||
│ ├── file-service.ts # File creation & output
|
│ ├── file-service.ts # File creation & output
|
||||||
│ ├── distill-service.ts # Content aggregation, link traversal
|
│ ├── distill-service.ts # Content aggregation, link traversal
|
||||||
│ ├── context-gathering-service.ts # Unified discovery orchestration
|
│ ├── context-gathering-service.ts # Unified discovery orchestration
|
||||||
│ └── task-dispatch-service.ts # tmux session & agent dispatch
|
│ ├── task-file-service.ts # Pending task files → OpenAugi/Tasks/ (watcher trigger)
|
||||||
|
│ └── task-dispatch-service.ts # tmux session & agent dispatch (deprecated)
|
||||||
├── types/
|
├── types/
|
||||||
│ ├── plugin.ts # Plugin interface
|
│ ├── plugin.ts # Plugin interface
|
||||||
│ ├── settings.ts # Settings interfaces & defaults
|
│ ├── settings.ts # Settings interfaces & defaults
|
||||||
|
|
@ -159,9 +161,26 @@ gatherContext(config: ContextGatheringConfig): Promise<GatheredContext>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### TaskDispatchService (`task-dispatch-service.ts`)
|
### TaskFileService (`task-file-service.ts`)
|
||||||
|
|
||||||
Manages tmux-based agent sessions dispatched from task notes.
|
Writes `status: pending` task files to `OpenAugi/Tasks/` via the vault API — the trigger contract consumed by the OpenAugi task watcher (`openaugi up` in the parent repo). No Node.js modules, so it works on mobile. See [AGENT_TASKS.md](AGENT_TASKS.md).
|
||||||
|
|
||||||
|
**Key Methods:**
|
||||||
|
- `createReviewPassTask()` - Queue "run the review pass"
|
||||||
|
- `createProcessDashboardTask()` - Queue "process the dashboard"
|
||||||
|
- `createDistillTask(context, sourceNote)` - Queue a distill of the given content (selection or note body)
|
||||||
|
|
||||||
|
**Contract:**
|
||||||
|
- File format defined in the parent repo's `src/openaugi/templates/task-template.md`
|
||||||
|
- Frontmatter: `status: pending`, `source_block_id: obsidian-plugin`, `source_note`
|
||||||
|
- Sections: `# title`, `## Context`, `## User instruction`, `## Task`, `## Human Todo`, `## Results`
|
||||||
|
- No `repo`/`working_dir` key — the watcher defaults the agent to the vault
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TaskDispatchService (`task-dispatch-service.ts`) — DEPRECATED
|
||||||
|
|
||||||
|
Manages tmux-based agent sessions dispatched from task notes. Deprecated: it launches tmux itself, bypassing the task watcher. Kept functional for existing users; to be removed over a release or two. Prefer `TaskFileService`.
|
||||||
|
|
||||||
**Key Methods:**
|
**Key Methods:**
|
||||||
- `launchOrAttach(file)` - If tmux session exists, attach; otherwise assemble context, create session, start agent
|
- `launchOrAttach(file)` - If tmux session exists, attach; otherwise assemble context, create session, start agent
|
||||||
|
|
@ -288,9 +307,12 @@ Commands are registered in `main.ts`:
|
||||||
| `openaugi-process-notes` | Process notes | Unified flow: linked notes |
|
| `openaugi-process-notes` | Process notes | Unified flow: linked notes |
|
||||||
| `openaugi-process-recent` | Process recent activity | Unified flow: recent activity |
|
| `openaugi-process-recent` | Process recent activity | Unified flow: recent activity |
|
||||||
| `openaugi-save-context` | Save context | Save raw aggregated content |
|
| `openaugi-save-context` | Save context | Save raw aggregated content |
|
||||||
| `task-dispatch-launch` | Task dispatch: Launch or attach | Launch or attach to agent tmux session |
|
| `augi-run-review-pass` | Augi: Run review pass | Write pending task file: "run the review pass" |
|
||||||
| `task-dispatch-kill` | Task dispatch: Kill session | Kill active tmux session for task note |
|
| `augi-process-dashboard` | Augi: Process dashboard | Write pending task file: "process the dashboard" |
|
||||||
| `task-dispatch-list` | Task dispatch: List active sessions | Show modal of all active task sessions |
|
| `augi-distill-selection` | Augi: Distill selection | Write pending distill task for selection/active note |
|
||||||
|
| `task-dispatch-launch` | Task dispatch: Launch or attach | Deprecated — launch or attach to agent tmux session |
|
||||||
|
| `task-dispatch-kill` | Task dispatch: Kill session | Deprecated — kill active tmux session for task note |
|
||||||
|
| `task-dispatch-list` | Task dispatch: List active sessions | Deprecated — show modal of all active task sessions |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,149 +1,170 @@
|
||||||
# Publishing a New Release
|
# Publishing a New Release
|
||||||
|
|
||||||
This guide walks through the complete process of publishing a new OpenAugi plugin release.
|
**name:** publish-release
|
||||||
|
**description:** How to cut and publish a new OpenAugi plugin version safely, keep it consistent with the Obsidian community store, and what to do if the plugin gets de-listed.
|
||||||
|
**when to use:** Any time you ship a new version of the plugin (features, fixes) or need to relist it in the community store.
|
||||||
|
|
||||||
## Pre-Release Checklist
|
---
|
||||||
|
|
||||||
Before starting the release process:
|
## TL;DR — use the script
|
||||||
|
|
||||||
- [ ] All features/fixes are merged to master
|
|
||||||
- [ ] **Automated tests pass** (`npm test`) — see [TESTING.md](TESTING.md)
|
|
||||||
- [ ] New features have test coverage (add to `tests/`)
|
|
||||||
- [ ] Code builds without errors (`npm run build`)
|
|
||||||
- [ ] Type checking passes (`npm run typecheck`)
|
|
||||||
- [ ] Manual testing completed in test vault (`/Users/chris/zk-for-testing`)
|
|
||||||
- [ ] CODEBASE_MAP.md is up to date with any architectural changes
|
|
||||||
|
|
||||||
## Release Process
|
|
||||||
|
|
||||||
### Step 1: Bump the Version
|
|
||||||
|
|
||||||
Update the version number in **both** files (they must match):
|
|
||||||
|
|
||||||
1. `manifest.json` - Update the `"version"` field
|
|
||||||
2. `package.json` - Update the `"version"` field
|
|
||||||
|
|
||||||
Use semantic versioning:
|
|
||||||
- **MAJOR** (1.0.0): Breaking changes
|
|
||||||
- **MINOR** (0.1.0): New features, backwards compatible
|
|
||||||
- **PATCH** (0.0.1): Bug fixes, backwards compatible
|
|
||||||
|
|
||||||
### Step 2: Commit the Version Bump
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add manifest.json package.json
|
# 1. Write the release notes first (they become the GitHub release body):
|
||||||
git commit -m "Bump version to X.Y.Z"
|
# docs/release-notes/X.Y.Z.md
|
||||||
git push origin master
|
# 2. Then run:
|
||||||
|
./scripts/release.sh X.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Create and Push the Git Tag
|
`release.sh` does the whole thing and **verifies it**: checks preconditions →
|
||||||
|
runs tests + build → bumps `manifest.json`, `package.json`, **and
|
||||||
|
`versions.json`** in lockstep → commits, pushes, tags → waits for CI → **publishes
|
||||||
|
the draft immediately** → verifies that the repo-root manifest, the released
|
||||||
|
manifest asset, and the tag all equal `X.Y.Z`. It stops loudly on any mismatch.
|
||||||
|
|
||||||
The tag **must** match the version in `manifest.json` exactly.
|
Everything below explains *why* it does what it does, and the manual fallback.
|
||||||
|
|
||||||
```bash
|
## The three things Obsidian actually checks
|
||||||
git tag -a X.Y.Z -m "X.Y.Z"
|
|
||||||
git push origin X.Y.Z
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
1. **`manifest.json` at the repo-root HEAD** — Obsidian reads only the `version`
|
||||||
- `-a` creates an [annotated tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging#_creating_tags) (required for Obsidian releases)
|
field here to decide "what's the latest version."
|
||||||
- `-m` specifies the tag message - must match the version number
|
2. **A GitHub release whose tag == that version** (no `v` prefix), with
|
||||||
|
`main.js`, `manifest.json`, and `styles.css` attached as assets. Obsidian
|
||||||
|
downloads the files from the *release*, not from the repo tree.
|
||||||
|
3. **`versions.json`** — maps each plugin version → minimum Obsidian version.
|
||||||
|
Used to decide whether a given user's Obsidian is new enough to be offered the
|
||||||
|
update.
|
||||||
|
|
||||||
### Step 4: Wait for GitHub Actions
|
If the root manifest advertises a version that has **no matching published
|
||||||
|
release**, the plugin is in an inconsistent state — and Obsidian's automated
|
||||||
|
validation can drop it from the store. Never leave that window open. (This is
|
||||||
|
what bit us on 0.6.0.)
|
||||||
|
|
||||||
The workflow will automatically:
|
## Version files — all THREE must move together
|
||||||
1. Build the plugin
|
|
||||||
2. Create a draft release with the built artifacts
|
|
||||||
|
|
||||||
Monitor progress at: https://github.com/bitsofchris/openaugi-obsidian-plugin/actions
|
| File | Field | Notes |
|
||||||
|
|------|-------|-------|
|
||||||
|
| `manifest.json` | `version` | source of truth for the release tag |
|
||||||
|
| `package.json` | `version` | keep in sync (npm/build hygiene) |
|
||||||
|
| `versions.json` | add `"X.Y.Z": "<minAppVersion>"` | **easy to forget — it's why updates/relisting break** |
|
||||||
|
|
||||||
### Step 5: Generate Release Notes
|
The `tests/version-consistency.test.ts` guard test fails the build if these ever
|
||||||
|
disagree. `npm test` therefore catches a forgotten `versions.json` bump before
|
||||||
|
you tag.
|
||||||
|
|
||||||
**Claude should generate release notes** by comparing the new tag to the previous release:
|
> Historical note: earlier versions of this doc listed only `manifest.json` and
|
||||||
|
> `package.json`. `versions.json` was missing entirely, which broke the
|
||||||
|
> `npm version` script and left the store metadata incomplete. Don't regress.
|
||||||
|
|
||||||
1. Look at all commits since the last release tag
|
## Pre-release checklist
|
||||||
2. Identify user-facing changes (features, fixes, improvements)
|
|
||||||
3. Create release notes in this format:
|
- [ ] Features/fixes merged to master; `git status` clean; local == `origin/master`
|
||||||
|
- [ ] `npm test` passes (includes the version-consistency guard) — see [TESTING.md](TESTING.md)
|
||||||
|
- [ ] `npm run build` clean (tsc + esbuild)
|
||||||
|
- [ ] Manually tested in a real vault (`npm run dev`, reload plugin, exercise the change)
|
||||||
|
- [ ] `docs/CODEBASE_MAP.md` / `README.md` updated for any behavior change
|
||||||
|
- [ ] Release notes written to `docs/release-notes/X.Y.Z.md`
|
||||||
|
|
||||||
|
## Release notes format
|
||||||
|
|
||||||
|
Write `docs/release-notes/X.Y.Z.md` before releasing. Claude: diff commits since
|
||||||
|
the previous tag and focus on user-facing changes.
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## TL;DR
|
# X.Y.Z — <one-line theme>
|
||||||
[One-sentence summary of the most important change(s)]
|
|
||||||
|
|
||||||
## What's New
|
## Added / Changed / Fixed / Deprecated
|
||||||
|
- <user-facing change>: <why the user cares>
|
||||||
|
|
||||||
### Features
|
## Notes
|
||||||
- [Feature 1]: [Brief description of what it does and why users care]
|
- <breaking changes or upgrade steps, if any>
|
||||||
- [Feature 2]: [Brief description]
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
- [Fix 1]: [What was broken and how it's fixed]
|
|
||||||
|
|
||||||
### Improvements
|
|
||||||
- [Improvement 1]: [What's better now]
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
[For any new features, include brief usage instructions]
|
|
||||||
|
|
||||||
## Upgrade Notes
|
|
||||||
|
|
||||||
[Any breaking changes or things users need to know when upgrading]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Write this out to a markdown file.
|
## Manual fallback (if you can't use the script)
|
||||||
|
|
||||||
### Step 6: Publish the Release
|
Only if `release.sh` can't run. Do the steps **in this order** and don't stop
|
||||||
|
before publishing:
|
||||||
1. Go to https://github.com/bitsofchris/openaugi-obsidian-plugin/releases
|
|
||||||
2. Find the draft release created by the workflow
|
|
||||||
3. Click **Edit**
|
|
||||||
4. Paste the generated release notes
|
|
||||||
5. Click **Publish release**
|
|
||||||
|
|
||||||
### Step 7: Update Documentation
|
|
||||||
|
|
||||||
Ensure all docs reflect the new release:
|
|
||||||
|
|
||||||
- [ ] `docs/CODEBASE_MAP.md` - Architecture changes
|
|
||||||
- [ ] `CLAUDE.md` - Any new development guidelines
|
|
||||||
- [ ] `README.md` - User-facing documentation (if applicable)
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Full release flow (example for version 0.4.0)
|
# 1. Bump ALL THREE (or run: npm version X.Y.Z — see note below)
|
||||||
npm test
|
# manifest.json .version, package.json .version, versions.json add "X.Y.Z":"<minAppVersion>"
|
||||||
npm run build
|
npm test && npm run build # guard test must be green
|
||||||
npm run typecheck
|
|
||||||
|
|
||||||
# Commit version bump
|
git add manifest.json package.json versions.json docs/release-notes/X.Y.Z.md
|
||||||
git add manifest.json package.json
|
git commit -m "Release X.Y.Z"
|
||||||
git commit -m "Bump version to 0.4.0"
|
|
||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
# Tag and push
|
git tag -a X.Y.Z -m "X.Y.Z" # annotated, no 'v' prefix
|
||||||
git tag -a 0.4.0 -m "0.4.0"
|
git push origin X.Y.Z # triggers .github/workflows/release.yml → draft
|
||||||
git push origin 0.4.0
|
|
||||||
|
|
||||||
# Then: Edit draft release on GitHub and publish
|
# 2. Wait for the workflow, then PUBLISH RIGHT AWAY (don't leave it a draft):
|
||||||
|
gh run watch "$(gh run list --workflow=release.yml --limit 1 --json databaseId --jq '.[0].databaseId')" --exit-status
|
||||||
|
gh release edit X.Y.Z --draft=false --latest --notes-file docs/release-notes/X.Y.Z.md
|
||||||
|
|
||||||
|
# 3. Verify root manifest, released asset, and tag all say X.Y.Z:
|
||||||
|
curl -sL https://raw.githubusercontent.com/bitsofchris/openaugi-obsidian-plugin/master/manifest.json | grep version
|
||||||
|
curl -sL https://github.com/bitsofchris/openaugi-obsidian-plugin/releases/download/X.Y.Z/manifest.json | grep version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> `npm version X.Y.Z` also works: `.npmrc` sets `tag-version-prefix=""` (so the
|
||||||
|
> tag has no `v`), and the `version` npm script runs `version-bump.mjs` which
|
||||||
|
> updates `manifest.json` + `versions.json`, while npm bumps `package.json`. It
|
||||||
|
> commits and tags for you — but it does **not** push or publish, so you still
|
||||||
|
> owe steps 1–3 above (push, wait, publish, verify). The script does all of it.
|
||||||
|
|
||||||
|
## Community-store listing health
|
||||||
|
|
||||||
|
The plugin is listed in Obsidian's store via
|
||||||
|
[`obsidianmd/obsidian-releases`](https://github.com/obsidianmd/obsidian-releases).
|
||||||
|
That repo's `community-plugins.json` is now an **automated mirror** of Obsidian's
|
||||||
|
internal list (commits read `chore: Mirror community plugins and themes`), so you
|
||||||
|
can't fix listing by PR'ing that file — it gets overwritten.
|
||||||
|
|
||||||
|
Check whether we're currently listed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sL https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-plugins.json | grep -i openaugi
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Listed** → core Settings → Community plugins → "Check for updates" works for users.
|
||||||
|
- **Not listed** → core update check can't see us; users must use **BRAT** or a
|
||||||
|
manual install. See the relisting steps below.
|
||||||
|
|
||||||
|
### If the plugin gets removed from the store
|
||||||
|
|
||||||
|
This happened on **2026-07-07** (removed in mirror commit `85db6f23`, bundled with
|
||||||
|
several unrelated plugins — an automated batch removal). The mirror commits carry
|
||||||
|
no per-plugin reason and Obsidian doesn't file an issue on your repo, so:
|
||||||
|
|
||||||
|
1. **Fix consistency first** (so a resubmit passes validation): run a clean
|
||||||
|
release with `release.sh` — root manifest version must equal a *published*
|
||||||
|
release with the three assets, `versions.json` present, `README`/`LICENSE`
|
||||||
|
present. (All true as of 0.6.0.)
|
||||||
|
2. **Ask Obsidian why + request reinstatement** — don't blindly resubmit an
|
||||||
|
auto-removal. Post in the Obsidian Discord **`#plugin-dev`** channel (fastest;
|
||||||
|
plugin-review team is active there) or the forum **Developers: Plugin & API**
|
||||||
|
category. Cite: repo URL, removal date `2026-07-07`, mirror commit `85db6f23`.
|
||||||
|
3. **If they tell you to resubmit:** the current path is the web portal at
|
||||||
|
**community.obsidian.md** → sign in with your Obsidian account → link GitHub →
|
||||||
|
Plugins → New plugin → enter the repo URL. (The old "PR to
|
||||||
|
community-plugins.json" flow is dead now that the file is bot-mirrored.)
|
||||||
|
4. **Gotcha — "An entry already exists for this repository":** because the old
|
||||||
|
entry is archived rather than deleted, resubmitting the same repo URL can be
|
||||||
|
rejected. Ask a moderator in `#plugin-dev` to delete the archived entry, then
|
||||||
|
resubmit.
|
||||||
|
|
||||||
|
Sources: Obsidian plugin submission docs; obsidian-releases repo (mirror
|
||||||
|
architecture); forum thread "Cannot resubmit plugin after archiving: 'An entry
|
||||||
|
already exists for this repository'".
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Tag already exists
|
**Tag already exists / wrong version tagged**
|
||||||
```bash
|
```bash
|
||||||
# Delete local tag
|
|
||||||
git tag -d X.Y.Z
|
git tag -d X.Y.Z
|
||||||
# Delete remote tag (if pushed)
|
git push origin --delete X.Y.Z # if already pushed
|
||||||
git push origin --delete X.Y.Z
|
# fix versions, re-run ./scripts/release.sh X.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
### Wrong version tagged
|
**Workflow failed** — `gh run view <id>`, fix, delete the tag, re-run the script.
|
||||||
1. Delete the incorrect tag (see above)
|
|
||||||
2. Fix the version in manifest.json/package.json
|
|
||||||
3. Commit and re-tag
|
|
||||||
|
|
||||||
### Workflow failed
|
**Release stuck as draft** — `gh release edit X.Y.Z --draft=false --latest`.
|
||||||
1. Check the Actions tab for error details
|
|
||||||
2. Fix the issue
|
|
||||||
3. Delete the tag and re-push after fixing
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
# Task Dispatch
|
# Task Dispatch
|
||||||
|
|
||||||
|
> **⚠️ Deprecated.** Task Dispatch launches tmux sessions directly from the
|
||||||
|
> plugin — a parallel execution path that bypasses the OpenAugi task
|
||||||
|
> watcher and will drift from it (different session names, duplicate repo
|
||||||
|
> settings). It still works, but it will be removed over a release or two.
|
||||||
|
> New workflows should use the **Augi commands** ([Agent Tasks](AGENT_TASKS.md)),
|
||||||
|
> which write task files to `OpenAugi/Tasks/` for the task watcher to
|
||||||
|
> execute — no tmux or macOS dependency in the plugin, and they work on
|
||||||
|
> mobile. If you rely on Task Dispatch without the Python watcher, nothing
|
||||||
|
> breaks today; plan to install [OpenAugi](https://github.com/bitsofchris/openaugi)
|
||||||
|
> and run `openaugi up` before it is removed.
|
||||||
|
|
||||||
Task Dispatch lets you launch AI coding agents directly from Obsidian task notes. Each task gets its own tmux session with full context from your vault — the task note body, all linked notes, and access to the OpenAugi MCP server for searching your vault.
|
Task Dispatch lets you launch AI coding agents directly from Obsidian task notes. Each task gets its own tmux session with full context from your vault — the task note body, all linked notes, and access to the OpenAugi MCP server for searching your vault.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
|
||||||
26
docs/release-notes/0.6.0.md
Normal file
26
docs/release-notes/0.6.0.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# 0.6.0 — Agent tasks via the vault (task-file trigger)
|
||||||
|
|
||||||
|
## Added — Augi commands
|
||||||
|
|
||||||
|
Three new commands queue work for the [OpenAugi task watcher](https://github.com/bitsofchris/openaugi) by writing a `status: pending` task file to `OpenAugi/Tasks/` through the Obsidian vault API. No shell-out, no HTTP, no Node modules — so they work on **Obsidian Mobile** against a synced vault.
|
||||||
|
|
||||||
|
- **Augi: Run review pass** — queues a full review pass (route new blocks → refresh views → surface Dashboard nominations).
|
||||||
|
- **Augi: Process dashboard** — queues execution of your Dashboard nomination answers only.
|
||||||
|
- **Augi: Distill selection** — queues a distill of the current editor selection (or the active note's body when nothing is selected) through the distill lens. The plugin is the scope selector; the selected text becomes the task's context verbatim.
|
||||||
|
|
||||||
|
The task-file format is shared with the `openaugi review` CLI and the `zzz:` capture grammar — one contract, defined in the parent repo's `templates/task-template.md`.
|
||||||
|
|
||||||
|
**Requires** the OpenAugi task watcher running against your vault (`openaugi up`).
|
||||||
|
|
||||||
|
## Deprecated — Task Dispatch
|
||||||
|
|
||||||
|
The legacy **Task Dispatch** feature (Launch/attach, Kill session, List active sessions) launches tmux sessions directly from the plugin, bypassing the task watcher. It is now **deprecated** and will be removed over the next release or two. It still works for existing users; the settings section and docs steer new use to the Augi commands + task watcher instead.
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- New `docs/AGENT_TASKS.md` covering the commands, the task-file contract, and the migration path.
|
||||||
|
- README and CODEBASE_MAP updated to lead with the task-file flow.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
New commands are additive and backward compatible; nothing existing was removed.
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"id": "openaugi",
|
"id": "openaugi",
|
||||||
"name": "OpenAugi",
|
"name": "OpenAugi",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"minAppVersion": "1.8.9",
|
"minAppVersion": "1.8.9",
|
||||||
"description": "Process information faster with augmented intelligence (AI for thinkers). Parse your voice notes into atomic notes, tasks, and summaries. Grab context from dataview queries and linked notes. De-duplicate and merge atomic ideas into a clean, organized vault.",
|
"description": "Process information faster with augmented intelligence (AI for thinkers). Parse your voice notes into atomic notes, tasks, and summaries. Grab context from dataview queries and linked notes. De-duplicate and merge atomic ideas into a clean, organized vault.",
|
||||||
"author": "Chris Lettieri",
|
"author": "Chris Lettieri",
|
||||||
"authorUrl": "https://bitsofchris.com",
|
"authorUrl": "https://bitsofchris.com",
|
||||||
"isDesktopOnly": true
|
"isDesktopOnly": false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "open-augi",
|
"name": "open-augi",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
114
scripts/release.sh
Executable file
114
scripts/release.sh
Executable file
|
|
@ -0,0 +1,114 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# release.sh — publish a new OpenAugi plugin version, safely and verifiably.
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/release.sh X.Y.Z
|
||||||
|
#
|
||||||
|
# Why this exists: doing releases by hand drifted manifest.json / package.json /
|
||||||
|
# versions.json out of sync (0.6.0 shipped with a stale package.json and no
|
||||||
|
# versions.json) and left a window where the repo advertised a version that had
|
||||||
|
# no published release — which is plausibly why the plugin was auto-removed from
|
||||||
|
# the community store. This script bumps all three files in lockstep, then does
|
||||||
|
# push → tag → wait-for-CI → PUBLISH → verify in one shot so that window never
|
||||||
|
# opens. See docs/PUBLISHING.md.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="bitsofchris/openaugi-obsidian-plugin"
|
||||||
|
die() { printf '\n\033[31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
|
||||||
|
ok() { printf '\033[32m✓ %s\033[0m\n' "$*"; }
|
||||||
|
step(){ printf '\n\033[1m▸ %s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
VERSION="${1:-}"
|
||||||
|
[[ -n "$VERSION" ]] || die "usage: ./scripts/release.sh X.Y.Z"
|
||||||
|
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "version must be X.Y.Z (no 'v' prefix): got '$VERSION'"
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
NOTES="docs/release-notes/${VERSION}.md"
|
||||||
|
|
||||||
|
# ── Preconditions ────────────────────────────────────────────────────────────
|
||||||
|
step "Preconditions"
|
||||||
|
command -v gh >/dev/null || die "gh CLI not found"
|
||||||
|
gh auth status >/dev/null 2>&1 || die "gh not authenticated (run: gh auth login)"
|
||||||
|
[[ "$(git branch --show-current)" == "master" ]] || die "must be on master"
|
||||||
|
[[ -z "$(git status --porcelain)" ]] || die "working tree not clean — commit or stash first"
|
||||||
|
git fetch -q origin master
|
||||||
|
[[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/master)" ]] || die "local master differs from origin/master — push/pull first"
|
||||||
|
git rev-parse "$VERSION" >/dev/null 2>&1 && die "tag $VERSION already exists locally"
|
||||||
|
[[ -f "$NOTES" ]] || die "release notes not found: $NOTES (write them first — they become the GitHub release body)"
|
||||||
|
ok "on master, clean, synced; notes present; tag is free"
|
||||||
|
|
||||||
|
# ── Quality gate ─────────────────────────────────────────────────────────────
|
||||||
|
step "Tests + build"
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
ok "tests + build pass"
|
||||||
|
|
||||||
|
# ── Bump all three version files in lockstep ─────────────────────────────────
|
||||||
|
step "Bump manifest.json / package.json / versions.json → $VERSION"
|
||||||
|
node - "$VERSION" <<'NODE'
|
||||||
|
const fs = require('fs');
|
||||||
|
const v = process.argv[2];
|
||||||
|
const write = (f, o) => fs.writeFileSync(f, JSON.stringify(o, null, '\t') + '\n');
|
||||||
|
|
||||||
|
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||||
|
manifest.version = v;
|
||||||
|
write('manifest.json', manifest);
|
||||||
|
|
||||||
|
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||||
|
pkg.version = v;
|
||||||
|
write('package.json', pkg);
|
||||||
|
|
||||||
|
const versions = JSON.parse(fs.readFileSync('versions.json', 'utf8'));
|
||||||
|
versions[v] = manifest.minAppVersion; // version -> min Obsidian version
|
||||||
|
write('versions.json', versions);
|
||||||
|
|
||||||
|
console.log(` manifest ${manifest.version} · package ${pkg.version} · versions[${v}]=${versions[v]}`);
|
||||||
|
NODE
|
||||||
|
# The guard test now proves the three files agree before we tag anything.
|
||||||
|
npx vitest run tests/version-consistency.test.ts
|
||||||
|
ok "version files consistent"
|
||||||
|
|
||||||
|
# ── Commit + push + tag ──────────────────────────────────────────────────────
|
||||||
|
step "Commit, push, tag"
|
||||||
|
git add manifest.json package.json versions.json "$NOTES"
|
||||||
|
git commit -q -m "Release $VERSION"
|
||||||
|
git push -q origin master
|
||||||
|
git tag -a "$VERSION" -m "$VERSION" # no 'v' prefix — Obsidian requires tag == manifest version
|
||||||
|
git push -q origin "$VERSION"
|
||||||
|
ok "pushed master + tag $VERSION (CI release workflow triggered)"
|
||||||
|
|
||||||
|
# ── Wait for CI to build the draft release ───────────────────────────────────
|
||||||
|
step "Waiting for release workflow"
|
||||||
|
sleep 5
|
||||||
|
RUN_ID="$(gh run list --workflow=release.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
|
||||||
|
gh run watch "$RUN_ID" --exit-status >/dev/null || die "release workflow failed — see: gh run view $RUN_ID"
|
||||||
|
ok "workflow succeeded (draft release created)"
|
||||||
|
|
||||||
|
# ── Publish immediately (close the inconsistency window) ─────────────────────
|
||||||
|
step "Publishing release $VERSION"
|
||||||
|
gh release edit "$VERSION" --draft=false --latest --notes-file "$NOTES" >/dev/null
|
||||||
|
ok "release published"
|
||||||
|
|
||||||
|
# ── Verify remote consistency ────────────────────────────────────────────────
|
||||||
|
step "Verifying"
|
||||||
|
RAW="https://raw.githubusercontent.com/${REPO}"
|
||||||
|
DL="https://github.com/${REPO}/releases/download/${VERSION}"
|
||||||
|
root_v="$(curl -sfL "$RAW/master/manifest.json" | node -e 'process.stdin.on("data",d=>console.log(JSON.parse(d).version))')"
|
||||||
|
rel_v="$(curl -sfL "$DL/manifest.json" | node -e 'process.stdin.on("data",d=>console.log(JSON.parse(d).version))')"
|
||||||
|
[[ "$root_v" == "$VERSION" ]] || die "root manifest on master is '$root_v', expected '$VERSION'"
|
||||||
|
[[ "$rel_v" == "$VERSION" ]] || die "released manifest asset is '$rel_v', expected '$VERSION'"
|
||||||
|
is_draft="$(gh release view "$VERSION" --json isDraft --jq .isDraft)"
|
||||||
|
[[ "$is_draft" == "false" ]] || die "release is still a draft"
|
||||||
|
ok "root manifest = release asset = tag = $VERSION; release is published"
|
||||||
|
|
||||||
|
# ── Store-listing health (informational) ─────────────────────────────────────
|
||||||
|
step "Community-store listing check"
|
||||||
|
if curl -sfL "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-plugins.json" | grep -qi '"openaugi"'; then
|
||||||
|
ok "plugin is currently listed in the Obsidian community store"
|
||||||
|
else
|
||||||
|
printf '\033[33m! Not in the community store list. Users update via BRAT or manual install.\n'
|
||||||
|
printf ' To relist, see docs/PUBLISHING.md → "If the plugin gets removed from the store".\033[0m\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n\033[32m✓ Released %s\033[0m → https://github.com/%s/releases/tag/%s\n' "$VERSION" "$REPO" "$VERSION"
|
||||||
186
src/main.ts
186
src/main.ts
|
|
@ -1,10 +1,13 @@
|
||||||
import { Plugin, Notice, TFile } from 'obsidian';
|
import { Plugin, Notice, TFile, Platform } from 'obsidian';
|
||||||
import { OpenAugiSettings, DEFAULT_SETTINGS } from './types/settings';
|
import { OpenAugiSettings, DEFAULT_SETTINGS } from './types/settings';
|
||||||
import { OpenAIService } from './services/openai-service';
|
import { OpenAIService } from './services/openai-service';
|
||||||
import { FileService } from './services/file-service';
|
import { FileService } from './services/file-service';
|
||||||
import { DistillService } from './services/distill-service';
|
import { DistillService } from './services/distill-service';
|
||||||
import { ContextGatheringService } from './services/context-gathering-service';
|
import { ContextGatheringService } from './services/context-gathering-service';
|
||||||
import { TaskDispatchService } from './services/task-dispatch-service';
|
import { TaskFileService, stripFrontmatter } from './services/task-file-service';
|
||||||
|
// Lazy-imported: TaskDispatchService uses Node.js modules (child_process, fs, path)
|
||||||
|
// that are unavailable on mobile. We dynamic-import it only on desktop.
|
||||||
|
import type { TaskDispatchService } from './services/task-dispatch-service';
|
||||||
import { OpenAugiSettingTab } from './ui/settings-tab';
|
import { OpenAugiSettingTab } from './ui/settings-tab';
|
||||||
import { LoadingIndicator } from './ui/loading-indicator';
|
import { LoadingIndicator } from './ui/loading-indicator';
|
||||||
import { SessionListModal } from './ui/session-list-modal';
|
import { SessionListModal } from './ui/session-list-modal';
|
||||||
|
|
@ -32,7 +35,8 @@ export default class OpenAugiPlugin extends Plugin {
|
||||||
fileService: FileService;
|
fileService: FileService;
|
||||||
distillService: DistillService;
|
distillService: DistillService;
|
||||||
contextGatheringService: ContextGatheringService;
|
contextGatheringService: ContextGatheringService;
|
||||||
taskDispatchService: TaskDispatchService;
|
taskFileService: TaskFileService;
|
||||||
|
taskDispatchService: TaskDispatchService | null;
|
||||||
loadingIndicator: LoadingIndicator;
|
loadingIndicator: LoadingIndicator;
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
|
|
@ -116,52 +120,96 @@ export default class OpenAugiPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Task dispatch commands
|
// Augi task-file commands — write a pending task to OpenAugi/Tasks/
|
||||||
|
// for the OpenAugi task watcher to pick up. Pure vault API, works on
|
||||||
|
// mobile too. These supersede the deprecated Task Dispatch commands.
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: 'task-dispatch-launch',
|
id: 'augi-run-review-pass',
|
||||||
name: 'Task dispatch: Launch or attach',
|
name: 'Augi: Run review pass',
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
const activeFile = this.app.workspace.getActiveFile();
|
await this.queueTaskFile(() => this.taskFileService.createReviewPassTask());
|
||||||
if (activeFile && activeFile.extension === 'md') {
|
|
||||||
await this.taskDispatchService.launchOrAttach(activeFile);
|
|
||||||
} else {
|
|
||||||
new Notice('Please open a task note first');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: 'task-dispatch-kill',
|
id: 'augi-process-dashboard',
|
||||||
name: 'Task dispatch: Kill session',
|
name: 'Augi: Process dashboard',
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
const activeFile = this.app.workspace.getActiveFile();
|
await this.queueTaskFile(() => this.taskFileService.createProcessDashboardTask());
|
||||||
if (activeFile && activeFile.extension === 'md') {
|
|
||||||
await this.taskDispatchService.killSession(activeFile);
|
|
||||||
} else {
|
|
||||||
new Notice('Please open a task note first');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: 'task-dispatch-list',
|
id: 'augi-distill-selection',
|
||||||
name: 'Task dispatch: List active sessions',
|
name: 'Augi: Distill selection',
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
const sessions = await this.taskDispatchService.listActiveSessions();
|
await this.distillSelection();
|
||||||
const modal = new SessionListModal(
|
}
|
||||||
this.app,
|
});
|
||||||
sessions,
|
|
||||||
async (session: TaskSession) => {
|
// Task dispatch commands — desktop only.
|
||||||
await this.taskDispatchService.openTerminal(session.tmuxSessionName);
|
// DEPRECATED: these launch tmux directly from the plugin, bypassing the
|
||||||
},
|
// task watcher. Kept working for existing users; will be removed over a
|
||||||
async (session: TaskSession) => {
|
// release or two. New workflows should use the Augi commands above.
|
||||||
await this.taskDispatchService.killSessionById(session.taskId);
|
if (!Platform.isMobile) {
|
||||||
new Notice(`Killed session: ${session.taskId}`);
|
this.addCommand({
|
||||||
|
id: 'task-dispatch-launch',
|
||||||
|
name: 'Task dispatch: Launch or attach',
|
||||||
|
callback: async () => {
|
||||||
|
if (!this.taskDispatchService) {
|
||||||
|
new Notice('Task dispatch is still loading, try again in a moment');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
modal.open();
|
if (activeFile && activeFile.extension === 'md') {
|
||||||
}
|
await this.taskDispatchService.launchOrAttach(activeFile);
|
||||||
});
|
} else {
|
||||||
|
new Notice('Please open a task note first');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'task-dispatch-kill',
|
||||||
|
name: 'Task dispatch: Kill session',
|
||||||
|
callback: async () => {
|
||||||
|
if (!this.taskDispatchService) {
|
||||||
|
new Notice('Task dispatch is still loading, try again in a moment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
|
if (activeFile && activeFile.extension === 'md') {
|
||||||
|
await this.taskDispatchService.killSession(activeFile);
|
||||||
|
} else {
|
||||||
|
new Notice('Please open a task note first');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'task-dispatch-list',
|
||||||
|
name: 'Task dispatch: List active sessions',
|
||||||
|
callback: async () => {
|
||||||
|
if (!this.taskDispatchService) {
|
||||||
|
new Notice('Task dispatch is still loading, try again in a moment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sessions = await this.taskDispatchService.listActiveSessions();
|
||||||
|
const modal = new SessionListModal(
|
||||||
|
this.app,
|
||||||
|
sessions,
|
||||||
|
async (session: TaskSession) => {
|
||||||
|
await this.taskDispatchService!.openTerminal(session.tmuxSessionName);
|
||||||
|
},
|
||||||
|
async (session: TaskSession) => {
|
||||||
|
await this.taskDispatchService!.killSessionById(session.taskId);
|
||||||
|
new Notice(`Killed session: ${session.taskId}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
modal.open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add settings tab
|
// Add settings tab
|
||||||
this.addSettingTab(new OpenAugiSettingTab(this.app, this));
|
this.addSettingTab(new OpenAugiSettingTab(this.app, this));
|
||||||
|
|
@ -195,11 +243,67 @@ export default class OpenAugiPlugin extends Plugin {
|
||||||
this.distillService,
|
this.distillService,
|
||||||
this.settings
|
this.settings
|
||||||
);
|
);
|
||||||
this.taskDispatchService = new TaskDispatchService(
|
this.taskFileService = new TaskFileService(this.app);
|
||||||
this.app,
|
|
||||||
this.settings,
|
// TaskDispatchService uses Node.js modules (child_process, fs, path)
|
||||||
this.distillService
|
// unavailable on mobile. Skip entirely on mobile.
|
||||||
);
|
this.taskDispatchService = null;
|
||||||
|
if (!Platform.isMobile) {
|
||||||
|
import('./services/task-dispatch-service')
|
||||||
|
.then(({ TaskDispatchService }) => {
|
||||||
|
this.taskDispatchService = new TaskDispatchService(
|
||||||
|
this.app,
|
||||||
|
this.settings,
|
||||||
|
this.distillService
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Fallback: Node.js modules unavailable in this environment.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a task-file writer and surface the result as a Notice.
|
||||||
|
* @param write Callback that writes the task file and returns its path
|
||||||
|
*/
|
||||||
|
private async queueTaskFile(write: () => Promise<string>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const path = await write();
|
||||||
|
const filename = path.split('/').pop();
|
||||||
|
new Notice(`Task queued: ${filename}\nThe OpenAugi task watcher will pick it up.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to write task file:', error);
|
||||||
|
new Notice('Failed to write task file. Check console for details.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue a distill task for the current selection, or the active note's
|
||||||
|
* body when nothing is selected. The chosen text becomes the task's
|
||||||
|
* ## Context section — the plugin acts as the scope selector.
|
||||||
|
*/
|
||||||
|
private async distillSelection(): Promise<void> {
|
||||||
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
|
const selection = this.app.workspace.activeEditor?.editor?.getSelection()?.trim() ?? '';
|
||||||
|
|
||||||
|
let context = selection;
|
||||||
|
if (!context) {
|
||||||
|
if (!activeFile || activeFile.extension !== 'md') {
|
||||||
|
new Notice('Select text or open a markdown note to distill');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = await this.app.vault.read(activeFile);
|
||||||
|
context = stripFrontmatter(raw).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
new Notice('Nothing to distill — the note is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceNote = activeFile?.basename ?? 'OpenAugi plugin';
|
||||||
|
await this.queueTaskFile(() => this.taskFileService.createDistillTask(context, sourceNote));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
160
src/services/task-file-service.ts
Normal file
160
src/services/task-file-service.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { App } from 'obsidian';
|
||||||
|
import { createFileWithCollisionHandling } from '../utils/filename-utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaskFileService — writes `status: pending` task files to OpenAugi/Tasks/
|
||||||
|
* via the Obsidian vault API. No shell-out, no HTTP, mobile-safe.
|
||||||
|
*
|
||||||
|
* The task file IS the trigger contract: the OpenAugi task watcher
|
||||||
|
* (`openaugi up` in the parent repo) polls OpenAugi/Tasks/ for pending
|
||||||
|
* files and launches an agent session for each. This service, the
|
||||||
|
* `openaugi review` CLI, and the zzz capture grammar all converge on the
|
||||||
|
* same file format, defined authoritatively in the parent repo at
|
||||||
|
* `src/openaugi/templates/task-template.md`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Where the task watcher looks for pending task files (vault-relative). */
|
||||||
|
export const TASKS_FOLDER = 'OpenAugi/Tasks';
|
||||||
|
|
||||||
|
/** Frontmatter marker identifying tasks written by this plugin. */
|
||||||
|
export const SOURCE_BLOCK_ID = 'obsidian-plugin';
|
||||||
|
|
||||||
|
export interface TaskFileOptions {
|
||||||
|
/** Human-readable task title — becomes the `# heading` and filename slug. */
|
||||||
|
title: string;
|
||||||
|
/** Verbatim content for the `## Context` section. */
|
||||||
|
context: string;
|
||||||
|
/** The literal user directive, block-quoted under `## User instruction`. */
|
||||||
|
instruction: string;
|
||||||
|
/** Self-contained description for the `## Task` section (the agent's real prompt). */
|
||||||
|
task: string;
|
||||||
|
/** Wikilink target for `source_note` frontmatter (note title, no brackets). */
|
||||||
|
sourceNote: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strip YAML frontmatter from note content, if present. */
|
||||||
|
export function stripFrontmatter(content: string): string {
|
||||||
|
return content.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lowercase, filename-safe slug for the task filename. */
|
||||||
|
export function taskFileSlug(title: string, maxLen = 50): string {
|
||||||
|
const slug = title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
return slug.slice(0, maxLen).replace(/-+$/, '') || 'task';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** YYYYMMDD-HHMMSS local timestamp, matching the `openaugi review` CLI. */
|
||||||
|
export function taskFileTimestamp(now: Date = new Date()): string {
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return (
|
||||||
|
`${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
|
||||||
|
`-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the full task-file markdown per the task-template contract.
|
||||||
|
* Section order matters: the watcher hydrates around `## Results` and the
|
||||||
|
* agent treats `## Task` as its prompt.
|
||||||
|
*/
|
||||||
|
export function buildTaskFileContent(opts: TaskFileOptions): string {
|
||||||
|
return `---
|
||||||
|
status: pending
|
||||||
|
source_block_id: ${SOURCE_BLOCK_ID}
|
||||||
|
source_note: "[[${opts.sourceNote}]]"
|
||||||
|
---
|
||||||
|
|
||||||
|
# ${opts.title}
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
${opts.context.trim()}
|
||||||
|
|
||||||
|
## User instruction
|
||||||
|
|
||||||
|
> ${opts.instruction}
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
${opts.task.trim()}
|
||||||
|
|
||||||
|
## Human Todo
|
||||||
|
|
||||||
|
## Results
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TaskFileService {
|
||||||
|
constructor(private app: App) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue a full review pass. The watcher-launched agent reads
|
||||||
|
* OpenAugi/AGENT/review-pass.md and runs the whole loop.
|
||||||
|
*/
|
||||||
|
async createReviewPassTask(now: Date = new Date()): Promise<string> {
|
||||||
|
const instruction = 'run the review pass';
|
||||||
|
return this.writeTask({
|
||||||
|
title: instruction,
|
||||||
|
context: `Triggered via the OpenAugi plugin command "Augi: Run review pass" at ${taskFileTimestamp(now)}.`,
|
||||||
|
instruction,
|
||||||
|
task: `Read OpenAugi/AGENT/review-pass.md and execute: ${instruction}.`,
|
||||||
|
sourceNote: 'OpenAugi plugin',
|
||||||
|
}, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue dashboard processing only — execute nomination answers,
|
||||||
|
* no new-block routing.
|
||||||
|
*/
|
||||||
|
async createProcessDashboardTask(now: Date = new Date()): Promise<string> {
|
||||||
|
const instruction = 'process the dashboard';
|
||||||
|
return this.writeTask({
|
||||||
|
title: instruction,
|
||||||
|
context: `Triggered via the OpenAugi plugin command "Augi: Process dashboard" at ${taskFileTimestamp(now)}.`,
|
||||||
|
instruction,
|
||||||
|
task: `Read OpenAugi/AGENT/review-pass.md and execute: ${instruction}.`,
|
||||||
|
sourceNote: 'OpenAugi plugin',
|
||||||
|
}, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue a distill task for the given content (current selection or
|
||||||
|
* active note body). The content goes into `## Context` verbatim; the
|
||||||
|
* agent applies the distill lens to exactly that scope.
|
||||||
|
*/
|
||||||
|
async createDistillTask(context: string, sourceNote: string, now: Date = new Date()): Promise<string> {
|
||||||
|
return this.writeTask({
|
||||||
|
title: `distill ${sourceNote}`,
|
||||||
|
context,
|
||||||
|
instruction: 'distill this per OpenAugi/AGENT/distill-lens.md',
|
||||||
|
task:
|
||||||
|
'Read OpenAugi/AGENT/distill-lens.md and apply the distill lens to the ' +
|
||||||
|
'content in the ## Context section above. That content is the entire ' +
|
||||||
|
'scope — do not pull in additional notes.',
|
||||||
|
sourceNote,
|
||||||
|
}, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ensure OpenAugi/Tasks/ exists and write the task file into it. */
|
||||||
|
private async writeTask(opts: TaskFileOptions, now: Date): Promise<string> {
|
||||||
|
// Create ancestors one level at a time — createFolder is not
|
||||||
|
// guaranteed to be recursive on all platforms.
|
||||||
|
let folder = '';
|
||||||
|
for (const segment of TASKS_FOLDER.split('/')) {
|
||||||
|
folder = folder ? `${folder}/${segment}` : segment;
|
||||||
|
if (!(await this.app.vault.adapter.exists(folder))) {
|
||||||
|
await this.app.vault.createFolder(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const filename = `${taskFileSlug(opts.title)}-${taskFileTimestamp(now)}.md`;
|
||||||
|
const content = buildTaskFileContent(opts);
|
||||||
|
return createFileWithCollisionHandling(
|
||||||
|
this.app.vault,
|
||||||
|
`${TASKS_FOLDER}/${filename}`,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,12 +2,15 @@ import { Plugin } from 'obsidian';
|
||||||
import { OpenAugiSettings } from './settings';
|
import { OpenAugiSettings } from './settings';
|
||||||
import { OpenAIService } from '../services/openai-service';
|
import { OpenAIService } from '../services/openai-service';
|
||||||
import { FileService } from '../services/file-service';
|
import { FileService } from '../services/file-service';
|
||||||
import { TaskDispatchService } from '../services/task-dispatch-service';
|
import type { TaskDispatchService } from '../services/task-dispatch-service';
|
||||||
|
import { TaskFileService } from '../services/task-file-service';
|
||||||
|
|
||||||
export default interface OpenAugiPlugin extends Plugin {
|
export default interface OpenAugiPlugin extends Plugin {
|
||||||
settings: OpenAugiSettings;
|
settings: OpenAugiSettings;
|
||||||
openAIService: OpenAIService;
|
openAIService: OpenAIService;
|
||||||
fileService: FileService;
|
fileService: FileService;
|
||||||
taskDispatchService: TaskDispatchService;
|
taskFileService: TaskFileService;
|
||||||
|
/** @deprecated Task Dispatch bypasses the task watcher — use TaskFileService. */
|
||||||
|
taskDispatchService: TaskDispatchService | null;
|
||||||
saveSettings(): Promise<void>;
|
saveSettings(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,8 @@ import { App, PluginSettingTab, Setting, Notice, DropdownComponent } from 'obsid
|
||||||
import type OpenAugiPlugin from '../types/plugin';
|
import type OpenAugiPlugin from '../types/plugin';
|
||||||
import { OpenAIService } from '../services/openai-service';
|
import { OpenAIService } from '../services/openai-service';
|
||||||
import { TerminalApp } from '../types/task-dispatch';
|
import { TerminalApp } from '../types/task-dispatch';
|
||||||
import { detectTmuxPath } from '../services/task-dispatch-service';
|
// Lazy-imported: detectTmuxPath lives in task-dispatch-service which uses
|
||||||
|
// Node.js modules unavailable on mobile.
|
||||||
|
|
||||||
export class OpenAugiSettingTab extends PluginSettingTab {
|
export class OpenAugiSettingTab extends PluginSettingTab {
|
||||||
plugin: OpenAugiPlugin;
|
plugin: OpenAugiPlugin;
|
||||||
|
|
@ -353,7 +354,15 @@ export class OpenAugiSettingTab extends PluginSettingTab {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Task Dispatch Settings Header
|
// Task Dispatch Settings Header
|
||||||
containerEl.createEl('h3', { text: 'Task Dispatch' });
|
containerEl.createEl('h3', { text: 'Task Dispatch (deprecated)' });
|
||||||
|
containerEl.createEl('p', {
|
||||||
|
text: 'Task Dispatch launches tmux sessions directly from the plugin and is '
|
||||||
|
+ 'deprecated — it will be removed in a future release. Prefer the '
|
||||||
|
+ '"Augi" commands (Run review pass, Process dashboard, Distill selection), '
|
||||||
|
+ 'which write task files to OpenAugi/Tasks/ for the OpenAugi task watcher '
|
||||||
|
+ 'to execute. The settings below only affect the legacy Task dispatch commands.',
|
||||||
|
cls: 'setting-item-description'
|
||||||
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('Terminal application')
|
.setName('Terminal application')
|
||||||
|
|
@ -387,6 +396,7 @@ export class OpenAugiSettingTab extends PluginSettingTab {
|
||||||
.addButton(button => button
|
.addButton(button => button
|
||||||
.setButtonText('Detect')
|
.setButtonText('Detect')
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
|
const { detectTmuxPath } = await import('../services/task-dispatch-service');
|
||||||
const found = await detectTmuxPath();
|
const found = await detectTmuxPath();
|
||||||
if (found) {
|
if (found) {
|
||||||
this.plugin.settings.taskDispatch.tmuxPath = found;
|
this.plugin.settings.taskDispatch.tmuxPath = found;
|
||||||
|
|
|
||||||
|
|
@ -167,11 +167,19 @@ describe('DistillService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filters content by date range', () => {
|
it('filters content by date range', () => {
|
||||||
const content = '# Daily Journal\n\n### 2026-02-25\nRecent entry\n\n### 2025-01-01\nVery old entry';
|
// Pin "now" so the fixed fixture dates are deterministic regardless of the
|
||||||
const filtered = (service as any).extractContentByDateRange(content, 30);
|
// real current date. extractContentByDateRange uses new Date() as "now".
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-02-28T12:00:00Z'));
|
||||||
|
try {
|
||||||
|
const content = '# Daily Journal\n\n### 2026-02-25\nRecent entry\n\n### 2025-01-01\nVery old entry';
|
||||||
|
const filtered = (service as any).extractContentByDateRange(content, 30);
|
||||||
|
|
||||||
expect(filtered).toContain('Recent entry');
|
expect(filtered).toContain('Recent entry');
|
||||||
expect(filtered).not.toContain('Very old entry');
|
expect(filtered).not.toContain('Very old entry');
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,5 +72,11 @@ export class FileSystemAdapter {
|
||||||
getBasePath(): string { return this.basePath; }
|
getBasePath(): string { return this.basePath; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Platform = {
|
||||||
|
isMobile: false,
|
||||||
|
isDesktop: true,
|
||||||
|
isDesktopApp: true,
|
||||||
|
};
|
||||||
|
|
||||||
export class MarkdownView {}
|
export class MarkdownView {}
|
||||||
export class WorkspaceLeaf {}
|
export class WorkspaceLeaf {}
|
||||||
|
|
|
||||||
189
tests/task-file-service.test.ts
Normal file
189
tests/task-file-service.test.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import {
|
||||||
|
TaskFileService,
|
||||||
|
TASKS_FOLDER,
|
||||||
|
buildTaskFileContent,
|
||||||
|
stripFrontmatter,
|
||||||
|
taskFileSlug,
|
||||||
|
taskFileTimestamp,
|
||||||
|
} from '../src/services/task-file-service';
|
||||||
|
import { createMockApp, MockApp } from './mocks/obsidian-mock';
|
||||||
|
|
||||||
|
// Mirrors FRONTMATTER_RE in the parent repo's task_watcher.py — the reader
|
||||||
|
// side of the task-file contract. If a file doesn't match this, the watcher
|
||||||
|
// never sees its frontmatter.
|
||||||
|
const WATCHER_FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
||||||
|
|
||||||
|
// ─── Pure helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('stripFrontmatter', () => {
|
||||||
|
it('removes a leading frontmatter block', () => {
|
||||||
|
expect(stripFrontmatter('---\nfoo: bar\n---\nBody text.')).toBe('Body text.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves content without frontmatter untouched', () => {
|
||||||
|
expect(stripFrontmatter('Just a note.')).toBe('Just a note.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not strip --- separators mid-document', () => {
|
||||||
|
const content = 'Intro.\n\n---\n\nMore.';
|
||||||
|
expect(stripFrontmatter(content)).toBe(content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('taskFileSlug', () => {
|
||||||
|
it('lowercases and hyphenates', () => {
|
||||||
|
expect(taskFileSlug('run the review pass')).toBe('run-the-review-pass');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips special characters', () => {
|
||||||
|
expect(taskFileSlug('distill Journal 2026-07-07!')).toBe('distill-journal-2026-07-07');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates long titles without a trailing hyphen', () => {
|
||||||
|
const slug = taskFileSlug('a'.repeat(45) + ' bcdef ghijk');
|
||||||
|
expect(slug.length).toBeLessThanOrEqual(50);
|
||||||
|
expect(slug.endsWith('-')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to "task" for empty input', () => {
|
||||||
|
expect(taskFileSlug('!!!')).toBe('task');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('taskFileTimestamp', () => {
|
||||||
|
it('formats as YYYYMMDD-HHMMSS', () => {
|
||||||
|
const ts = taskFileTimestamp(new Date(2026, 6, 7, 9, 5, 3));
|
||||||
|
expect(ts).toBe('20260707-090503');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── buildTaskFileContent ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('buildTaskFileContent', () => {
|
||||||
|
const content = buildTaskFileContent({
|
||||||
|
title: 'run the review pass',
|
||||||
|
context: 'Triggered via the plugin.',
|
||||||
|
instruction: 'run the review pass',
|
||||||
|
task: 'Read OpenAugi/AGENT/review-pass.md and execute: run the review pass.',
|
||||||
|
sourceNote: 'OpenAugi plugin',
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produces frontmatter the watcher can parse, with status pending', () => {
|
||||||
|
const match = content.match(WATCHER_FRONTMATTER_RE);
|
||||||
|
expect(match).toBeTruthy();
|
||||||
|
expect(match![1]).toContain('status: pending');
|
||||||
|
expect(match![1]).toContain('source_block_id: obsidian-plugin');
|
||||||
|
expect(match![1]).toContain('source_note: "[[OpenAugi plugin]]"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains all template sections in order', () => {
|
||||||
|
const sections = [
|
||||||
|
'# run the review pass',
|
||||||
|
'## Context',
|
||||||
|
'## User instruction',
|
||||||
|
'## Task',
|
||||||
|
'## Human Todo',
|
||||||
|
'## Results',
|
||||||
|
];
|
||||||
|
let lastIndex = -1;
|
||||||
|
for (const section of sections) {
|
||||||
|
const idx = content.indexOf(section);
|
||||||
|
expect(idx, `missing section: ${section}`).toBeGreaterThan(lastIndex);
|
||||||
|
lastIndex = idx;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('block-quotes the user instruction', () => {
|
||||||
|
expect(content).toContain('> run the review pass');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the context verbatim under ## Context', () => {
|
||||||
|
const multiline = buildTaskFileContent({
|
||||||
|
title: 't',
|
||||||
|
context: 'Line one.\n\n- bullet\n- another',
|
||||||
|
instruction: 'i',
|
||||||
|
task: 'do it',
|
||||||
|
sourceNote: 'Note',
|
||||||
|
});
|
||||||
|
expect(multiline).toContain('## Context\n\nLine one.\n\n- bullet\n- another\n\n## User instruction');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── TaskFileService (fs-backed vault) ───────────────────────────────────────
|
||||||
|
|
||||||
|
describe('TaskFileService', () => {
|
||||||
|
let vaultRoot: string;
|
||||||
|
let app: MockApp;
|
||||||
|
let service: TaskFileService;
|
||||||
|
const now = new Date(2026, 6, 7, 14, 30, 0);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vaultRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'openaugi-taskfile-'));
|
||||||
|
app = createMockApp(vaultRoot);
|
||||||
|
service = new TaskFileService(app as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(vaultRoot, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function readTask(relPath: string): string {
|
||||||
|
return fs.readFileSync(path.join(vaultRoot, relPath), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
it('creates the OpenAugi/Tasks folder if missing', async () => {
|
||||||
|
expect(fs.existsSync(path.join(vaultRoot, TASKS_FOLDER))).toBe(false);
|
||||||
|
await service.createReviewPassTask(now);
|
||||||
|
expect(fs.existsSync(path.join(vaultRoot, TASKS_FOLDER))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes a pending review-pass task with the expected filename', async () => {
|
||||||
|
const taskPath = await service.createReviewPassTask(now);
|
||||||
|
|
||||||
|
expect(taskPath).toBe(`${TASKS_FOLDER}/run-the-review-pass-20260707-143000.md`);
|
||||||
|
const content = readTask(taskPath);
|
||||||
|
expect(content.match(WATCHER_FRONTMATTER_RE)![1]).toContain('status: pending');
|
||||||
|
expect(content).toContain('> run the review pass');
|
||||||
|
expect(content).toContain('Read OpenAugi/AGENT/review-pass.md and execute: run the review pass.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes a process-dashboard task with its own instruction', async () => {
|
||||||
|
const taskPath = await service.createProcessDashboardTask(now);
|
||||||
|
|
||||||
|
expect(taskPath).toBe(`${TASKS_FOLDER}/process-the-dashboard-20260707-143000.md`);
|
||||||
|
const content = readTask(taskPath);
|
||||||
|
expect(content).toContain('> process the dashboard');
|
||||||
|
expect(content).toContain('Read OpenAugi/AGENT/review-pass.md and execute: process the dashboard.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes a distill task with the given content as ## Context', async () => {
|
||||||
|
const selection = 'Idea one about capture.\n\nIdea two about routing.';
|
||||||
|
const taskPath = await service.createDistillTask(selection, 'Journal 2026-07-07', now);
|
||||||
|
|
||||||
|
expect(taskPath).toBe(`${TASKS_FOLDER}/distill-journal-2026-07-07-20260707-143000.md`);
|
||||||
|
const content = readTask(taskPath);
|
||||||
|
expect(content).toContain('source_note: "[[Journal 2026-07-07]]"');
|
||||||
|
expect(content).toContain(`## Context\n\n${selection}\n\n## User instruction`);
|
||||||
|
expect(content).toContain('> distill this per OpenAugi/AGENT/distill-lens.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not overwrite when two tasks land in the same second', async () => {
|
||||||
|
const first = await service.createReviewPassTask(now);
|
||||||
|
const second = await service.createReviewPassTask(now);
|
||||||
|
|
||||||
|
expect(second).not.toBe(first);
|
||||||
|
expect(fs.existsSync(path.join(vaultRoot, first))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(vaultRoot, second))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never sets a repo/working_dir key — the watcher defaults to the vault', async () => {
|
||||||
|
const taskPath = await service.createReviewPassTask(now);
|
||||||
|
const frontmatter = readTask(taskPath).match(WATCHER_FRONTMATTER_RE)![1];
|
||||||
|
expect(frontmatter).not.toContain('repo:');
|
||||||
|
expect(frontmatter).not.toContain('working_dir');
|
||||||
|
});
|
||||||
|
});
|
||||||
45
tests/version-consistency.test.ts
Normal file
45
tests/version-consistency.test.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// Guard test for the release contract. Obsidian resolves the "latest version"
|
||||||
|
// from manifest.json at the repo root, then downloads the GitHub release whose
|
||||||
|
// tag equals that version. If manifest.json / package.json / versions.json ever
|
||||||
|
// disagree, releases go out inconsistent — which is how the 0.6.0 release shipped
|
||||||
|
// with a stale package.json and a MISSING versions.json, and (plausibly) how the
|
||||||
|
// plugin got auto-removed from the community store. This test makes that drift a
|
||||||
|
// red build instead of a production incident. See docs/PUBLISHING.md.
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const readJson = (rel: string) =>
|
||||||
|
JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
|
||||||
|
|
||||||
|
const SEMVER = /^\d+\.\d+\.\d+$/; // x.y.z only — no "v" prefix, no pre-release suffix
|
||||||
|
|
||||||
|
describe('version consistency (release contract)', () => {
|
||||||
|
const manifest = readJson('manifest.json');
|
||||||
|
const pkg = readJson('package.json');
|
||||||
|
const versions = readJson('versions.json');
|
||||||
|
|
||||||
|
it('manifest.json version is valid semver (x.y.z, no v prefix)', () => {
|
||||||
|
expect(manifest.version).toMatch(SEMVER);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('package.json version matches manifest.json', () => {
|
||||||
|
expect(pkg.version).toBe(manifest.version);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('versions.json contains the current manifest version', () => {
|
||||||
|
expect(Object.keys(versions)).toContain(manifest.version);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('versions.json maps the current version to manifest.minAppVersion', () => {
|
||||||
|
expect(versions[manifest.version]).toBe(manifest.minAppVersion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('every versions.json key is valid semver', () => {
|
||||||
|
for (const v of Object.keys(versions)) {
|
||||||
|
expect(v, `versions.json key "${v}"`).toMatch(SEMVER);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"0.6.0": "1.8.9"
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue