Compare commits

...

6 commits

Author SHA1 Message Date
Chris Lettieri
cc62c4d591 release tooling: scripts/release.sh + version-consistency guard test
Codify the release process so we stop drifting version files and stop
leaving the repo advertising a version with no published release (the
inconsistency that plausibly got the plugin auto-removed from the store).

- scripts/release.sh: one-shot, self-verifying release. Bumps manifest.json,
  package.json AND versions.json in lockstep, runs tests+build, pushes,
  tags (no v prefix), waits for CI, PUBLISHES the draft immediately, then
  verifies root manifest == released asset == tag. Includes a store-listing
  health check.
- tests/version-consistency.test.ts: guard test — fails the build if the
  three version files ever disagree (catches a forgotten versions.json bump
  before tagging).
- docs/PUBLISHING.md: rewritten script-first; adds versions.json (was
  omitted), the publish-immediately rule, and a 'if de-listed from the
  store' runbook (ask #plugin-dev, community.obsidian.md portal, the
  'entry already exists' gotcha).
- CLAUDE.md: quick-summary now points at the script and the three files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:04:53 -04:00
Chris Lettieri
09892d2ef3 chore: sync package.json to 0.6.0; add versions.json
package.json version drifted from manifest; add the missing versions.json
(version -> minAppVersion) that version-bump.mjs and community-store
submission both require. Neither affects Obsidian's update check for the
current install path, but both were inconsistent locations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:48:24 -04:00
Chris Lettieri
cac28bd98d docs: 0.6.0 release notes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 08:34:12 -04:00
Chris Lettieri
6ff04f0f33 chore: bump version to 0.6.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 08:33:34 -04:00
Chris Lettieri
cbc0c1b471 test: make distill date-range filter test deterministic
The 'filters content by date range' test used fixed fixture dates
(### 2026-02-25) inside a 30-day window, so it passed only while the
real clock was within 30 days of that date and began failing as the
current date advanced. extractContentByDateRange is correct; it uses
new Date() as "now". Pin the system clock with vi.setSystemTime so the
fixed fixture dates evaluate deterministically regardless of the wall
clock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:27:40 -04:00
Chris Lettieri
521399efd3 M3b: Augi task-file commands; deprecate Task Dispatch
Add TaskFileService, which writes `status: pending` task files to
OpenAugi/Tasks/ via the Obsidian vault API — no shell-out, no HTTP, no
Node modules, so it works on mobile too. The file format mirrors the
parent repo's templates/task-template.md contract consumed by
task_watcher.py.

New commands (all platforms):
- Augi: Run review pass       -> "run the review pass"
- Augi: Process dashboard     -> "process the dashboard"
- Augi: Distill selection     -> selection/active-note body becomes the
                                 ## Context; distill-lens instruction

Deprecate the legacy Task Dispatch feature (it launches tmux itself,
bypassing the watcher). Kept functional for community users without the
Python watcher; steer new use to task files via docs banner, settings UI
label, and a @deprecated marker. To be removed over a release or two.

Docs: new docs/AGENT_TASKS.md; README + CODEBASE_MAP updated to lead with
the task-file flow. Tests: 18 new, including a watcher-frontmatter-regex
compat check and same-second collision handling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 08:26:30 -04:00
18 changed files with 951 additions and 176 deletions

View file

@ -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

View file

@ -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
View 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.

View file

@ -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 |
--- ---

View file

@ -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 13 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

View file

@ -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

View 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.

View file

@ -1,7 +1,7 @@
{ {
"id": "openaugi", "id": "openaugi",
"name": "OpenAugi", "name": "OpenAugi",
"version": "0.5.2", "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",

View file

@ -1,6 +1,6 @@
{ {
"name": "open-augi", "name": "open-augi",
"version": "0.5.2", "version": "0.6.0",
"description": "", "description": "",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

114
scripts/release.sh Executable file
View 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"

View file

@ -4,6 +4,7 @@ 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 { TaskFileService, stripFrontmatter } from './services/task-file-service';
// Lazy-imported: TaskDispatchService uses Node.js modules (child_process, fs, path) // Lazy-imported: TaskDispatchService uses Node.js modules (child_process, fs, path)
// that are unavailable on mobile. We dynamic-import it only on desktop. // that are unavailable on mobile. We dynamic-import it only on desktop.
import type { TaskDispatchService } from './services/task-dispatch-service'; import type { TaskDispatchService } from './services/task-dispatch-service';
@ -34,6 +35,7 @@ export default class OpenAugiPlugin extends Plugin {
fileService: FileService; fileService: FileService;
distillService: DistillService; distillService: DistillService;
contextGatheringService: ContextGatheringService; contextGatheringService: ContextGatheringService;
taskFileService: TaskFileService;
taskDispatchService: TaskDispatchService | null; taskDispatchService: TaskDispatchService | null;
loadingIndicator: LoadingIndicator; loadingIndicator: LoadingIndicator;
@ -118,7 +120,37 @@ export default class OpenAugiPlugin extends Plugin {
} }
}); });
// Task dispatch commands — desktop only // 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({
id: 'augi-run-review-pass',
name: 'Augi: Run review pass',
callback: async () => {
await this.queueTaskFile(() => this.taskFileService.createReviewPassTask());
}
});
this.addCommand({
id: 'augi-process-dashboard',
name: 'Augi: Process dashboard',
callback: async () => {
await this.queueTaskFile(() => this.taskFileService.createProcessDashboardTask());
}
});
this.addCommand({
id: 'augi-distill-selection',
name: 'Augi: Distill selection',
callback: async () => {
await this.distillSelection();
}
});
// Task dispatch commands — desktop only.
// DEPRECATED: these launch tmux directly from the plugin, bypassing the
// task watcher. Kept working for existing users; will be removed over a
// release or two. New workflows should use the Augi commands above.
if (!Platform.isMobile) { if (!Platform.isMobile) {
this.addCommand({ this.addCommand({
id: 'task-dispatch-launch', id: 'task-dispatch-launch',
@ -211,6 +243,7 @@ export default class OpenAugiPlugin extends Plugin {
this.distillService, this.distillService,
this.settings this.settings
); );
this.taskFileService = new TaskFileService(this.app);
// TaskDispatchService uses Node.js modules (child_process, fs, path) // TaskDispatchService uses Node.js modules (child_process, fs, path)
// unavailable on mobile. Skip entirely on mobile. // unavailable on mobile. Skip entirely on mobile.
@ -230,6 +263,49 @@ export default class OpenAugiPlugin extends Plugin {
} }
} }
/**
* 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));
}
/** /**
* Open a file in a new tab * Open a file in a new tab
* @param filePath The path to the file to open * @param filePath The path to the file to open

View 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
);
}
}

View file

@ -3,11 +3,14 @@ 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 type { 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;
taskFileService: TaskFileService;
/** @deprecated Task Dispatch bypasses the task watcher — use TaskFileService. */
taskDispatchService: TaskDispatchService | null; taskDispatchService: TaskDispatchService | null;
saveSettings(): Promise<void>; saveSettings(): Promise<void>;
} }

View file

@ -354,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')

View file

@ -167,11 +167,19 @@ describe('DistillService', () => {
}); });
it('filters content by date range', () => { it('filters content by date range', () => {
// Pin "now" so the fixed fixture dates are deterministic regardless of the
// 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 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); 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();
}
}); });
}); });

View 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');
});
});

View 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
View file

@ -0,0 +1,3 @@
{
"0.6.0": "1.8.9"
}