Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ claude --plugin-dir ./apps/hook
| `PLANNOTATOR_ORIGIN` | Explicit agent-origin override at the top of the detection chain. Valid values: `claude-code`, `opencode`, `codex`, `copilot-cli`, `gemini-cli`, `pi`. Invalid values silently fall through to env-based detection. Unset by default. |
| `PLANNOTATOR_JINA` | Set to `0` / `false` to disable Jina Reader for URL annotation, or `1` / `true` to enable. Default: enabled. Can also be set via `~/.plannotator/config.json` (`{ "jina": false }`) or per-invocation via `--no-jina`. |
| `JINA_API_KEY` | Optional Jina Reader API key for higher rate limits (500 RPM vs 20 RPM unauthenticated). Free keys include 10M tokens. |
| `PLANNOTATOR_DATA_DIR` | Override the base data directory. Supports `~` expansion. Default: `~/.plannotator`. All data (plans, history, drafts, config, hooks, sessions, debug logs, IPC registry) is stored under this directory. |
| `PLANNOTATOR_VERIFY_ATTESTATION` | **Read by the install scripts only**, not by the runtime binary. Set to `1` / `true` to have `scripts/install.sh` / `install.ps1` / `install.cmd` run `gh attestation verify` on every install. Off by default. Can also be set persistently via `~/.plannotator/config.json` (`{ "verifyAttestation": true }`) or per-invocation via `--verify-attestation`. Requires `gh` installed and authenticated. |

**Config-only settings (`~/.plannotator/config.json`)**: Some settings have no env-var equivalent and are toggled by editing the config file directly:
Expand Down
4 changes: 2 additions & 2 deletions apps/opencode-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

import { type Plugin, tool } from "@opencode-ai/plugin";
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
import { homedir } from "os";
import path from "path";
import { getPlannotatorDataDir } from "@plannotator/shared/data-dir";

// OpenCode's @hono/node-server patches global.Response with a polyfill that
// Bun.serve() doesn't accept (it checks native type tags, not instanceof).
Expand Down Expand Up @@ -135,7 +135,7 @@ interface PlanEdit {
* the agent never sees or touches this file directly.
*/
export function getPlanBackingPath(project: string): string {
return path.join(homedir(), ".plannotator", "active", project, "_active-plan.md");
return path.join(getPlannotatorDataDir(), "active", project, "_active-plan.md");
}

/**
Expand Down
7 changes: 4 additions & 3 deletions apps/opencode-plugin/submit-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,15 @@ describe("formatWithLineNumbers", () => {
// ── getPlanBackingPath ─────────────────────────────────────────────────────

describe("getPlanBackingPath", () => {
test("returns path inside ~/.plannotator/active/{project}/_active-plan.md", () => {
test("returns path inside data dir active/{project}/_active-plan.md", () => {
const result = getPlanBackingPath("myproject");
expect(result).toBe(path.join(homedir(), ".plannotator", "active", "myproject", "_active-plan.md"));
const dataDir = process.env.PLANNOTATOR_DATA_DIR || path.join(homedir(), ".plannotator");
expect(result).toBe(path.join(dataDir, "active", "myproject", "_active-plan.md"));
});

test("uses the provided project name as the directory segment", () => {
const result = getPlanBackingPath("some-project");
expect(result).toContain(path.join(".plannotator", "active", "some-project"));
expect(result).toContain(path.join("active", "some-project"));
expect(result).toContain("_active-plan.md");
});
});
4 changes: 3 additions & 1 deletion apps/pi-extension/vendor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ cd "$(dirname "$0")"
rm -rf generated
mkdir -p generated generated/ai/providers

for f in feedback-templates prompts review-core jj-core vcs-core review-args storage draft project pr-types pr-provider pr-stack pr-github pr-gitlab checklist integrations-common repo reference-common favicon code-file resolve-file config external-annotation agent-jobs worktree worktree-pool html-to-markdown url-to-markdown tour annotate-args at-reference pfm-reminder improvement-hooks code-nav; do
for f in feedback-templates prompts review-core jj-core vcs-core review-args storage draft project pr-types pr-provider pr-stack pr-github pr-gitlab checklist integrations-common repo reference-common favicon code-file resolve-file config external-annotation agent-jobs worktree worktree-pool html-to-markdown url-to-markdown tour annotate-args at-reference pfm-reminder improvement-hooks code-nav data-dir; do
src="../../packages/shared/$f.ts"
printf '// @generated — DO NOT EDIT. Source: packages/shared/%s.ts\n' "$f" | cat - "$src" > "generated/$f.ts"
done
Expand All @@ -19,6 +19,7 @@ for f in agent-review-message codex-review claude-review path-utils; do
| sed 's|from "./vcs"|from "./review-core.js"|' \
| sed 's|from "./pr"|from "./pr-provider.js"|' \
| sed 's|from "./path-utils"|from "./path-utils.js"|' \
| sed 's|from "@plannotator/shared/data-dir"|from "./data-dir"|' \
> "generated/$f.ts"
done

Expand All @@ -31,6 +32,7 @@ for f in tour-review; do
| sed 's|from "\.\./pr"|from "./pr-provider.js"|' \
| sed 's|from "\.\./agent-review-message"|from "./agent-review-message.js"|' \
| sed 's|from "@plannotator/shared/tour"|from "./tour.js"|' \
| sed 's|from "@plannotator/shared/data-dir"|from "./data-dir"|' \
> "generated/$f.ts"
done

Expand Down
13 changes: 7 additions & 6 deletions apps/skills/plannotator-compound/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ Research integrity is paramount — every file must be read, no skipping.

Before starting the analysis, determine which data source is available.

1. **Plannotator mode (first-class)** — Check `~/.plannotator/plans/`. If it
exists and contains `*-denied.md` files, use this mode. The entire workflow
below is written for Plannotator data.
1. **Plannotator mode (first-class)** — Determine the Plannotator data directory:
use `$PLANNOTATOR_DATA_DIR` if set, otherwise `~/.plannotator`. Check the
`plans/` subdirectory there. If it exists and contains `*-denied.md` files,
use this mode. The entire workflow below is written for Plannotator data.

2. **Claude Code fallback mode** — If the Plannotator archive is absent or
contains no denied plans, check `~/.claude/projects/`. If present, read
Expand Down Expand Up @@ -54,7 +55,7 @@ In either mode, proceed to Previous Report Detection below.
After locating the plans directory, check for existing reports:

```
ls ~/.plannotator/plans/compound-planning-report*.html
ls ${PLANNOTATOR_DATA_DIR:-~/.plannotator}/plans/compound-planning-report*.html
```

Reports follow a versioned naming scheme:
Expand Down Expand Up @@ -518,10 +519,10 @@ every future planning session automatically.
The hook file lives at:

```
~/.plannotator/hooks/compound/enterplanmode-improve-hook.txt
${PLANNOTATOR_DATA_DIR:-~/.plannotator}/hooks/compound/enterplanmode-improve-hook.txt
```

Create the `~/.plannotator/hooks/compound/` directory if it doesn't exist.
Create the `hooks/compound/` directory inside the data directory if it doesn't exist.

The file contents should be the corrective prompt instructions from Phase 3 —
the same numbered list that appears in section 7 of the HTML report. Write them
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Read this file only when the user does **not** have a usable Plannotator archive.

This is the secondary path for ordinary Claude Code users whose denial history
exists in `~/.claude/projects/` rather than `~/.plannotator/plans/`.
exists in `~/.claude/projects/` rather than the Plannotator plans directory (`$PLANNOTATOR_DATA_DIR/plans/` or `~/.plannotator/plans/`).

The goal is the same as the main skill:

Expand Down Expand Up @@ -107,7 +107,7 @@ does not contain those artifacts.
### Previous Report Detection In Fallback Mode

Previous report detection still applies. Check the user's home directory or
`~/.plannotator/plans/` for existing `compound-planning-report*.html` files. If
the Plannotator plans directory (`${PLANNOTATOR_DATA_DIR:-~/.plannotator}/plans/`) for existing `compound-planning-report*.html` files. If
found, offer the same incremental vs full choice as Plannotator mode. In
incremental mode, filter the parser output by timestamp rather than by filename
date — use the `timestamp` field in each JSON record.
Expand Down Expand Up @@ -246,7 +246,7 @@ feedback and therefore could not contribute to the pattern analysis.
### Versioned Report Naming

Versioned naming (`v2`, `v3`, etc.) applies to fallback mode too. Save reports
to `~/.plannotator/plans/` (create the directory if it doesn't exist) so that
to the Plannotator plans directory (`${PLANNOTATOR_DATA_DIR:-~/.plannotator}/plans/`, create the directory if it doesn't exist) so that
all compound planning reports live in the same location regardless of data source.

## Summary In Fallback Mode
Expand Down
4 changes: 3 additions & 1 deletion apps/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { createCookieProxy } from "./cookie-proxy";
import { PanelManager } from "./panel-manager";
import { setActiveProxyPort, registerEditorAnnotationCommand } from "./editor-annotations";

const IPC_REGISTRY = path.join(os.homedir(), ".plannotator", "vscode-ipc.json");
import { getPlannotatorDataDir } from "../../../packages/shared/data-dir";

const IPC_REGISTRY = path.join(getPlannotatorDataDir(), "vscode-ipc.json");

function readIpcRegistry(): Record<string, number> {
try {
Expand Down
3 changes: 2 additions & 1 deletion packages/server/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { $ } from "bun";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";
import { getPlannotatorDataDir } from "@plannotator/shared/data-dir";

const IPC_REGISTRY = path.join(os.homedir(), ".plannotator", "vscode-ipc.json");
const IPC_REGISTRY = path.join(getPlannotatorDataDir(), "vscode-ipc.json");

/**
* Common "no-op" values for $BROWSER used by headless/background environments
Expand Down
10 changes: 6 additions & 4 deletions packages/server/codex-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@
*/

import { join } from "node:path";
import { homedir, tmpdir } from "node:os";
import { tmpdir } from "node:os";
import { appendFile, mkdir, unlink, writeFile, readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { toRelativePath } from "./path-utils";
import { getPlannotatorDataDir } from "@plannotator/shared/data-dir";

// ---------------------------------------------------------------------------
// Debug log — only active when PLANNOTATOR_DEBUG is set
// ---------------------------------------------------------------------------

const DATA_DIR = getPlannotatorDataDir();
const DEBUG_ENABLED = !!process.env.PLANNOTATOR_DEBUG;
const DEBUG_LOG_PATH = join(homedir(), ".plannotator", "codex-review-debug.log");
const DEBUG_LOG_PATH = join(DATA_DIR, "codex-review-debug.log");

async function debugLog(label: string, data?: unknown): Promise<void> {
if (!DEBUG_ENABLED) return;
try {
await mkdir(join(homedir(), ".plannotator"), { recursive: true });
await mkdir(DATA_DIR, { recursive: true });
const timestamp = new Date().toISOString();
const line = data !== undefined
? `[${timestamp}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}\n`
Expand Down Expand Up @@ -78,7 +80,7 @@ const CODEX_REVIEW_SCHEMA = JSON.stringify({
additionalProperties: false,
});

const SCHEMA_DIR = join(homedir(), ".plannotator");
const SCHEMA_DIR = DATA_DIR;
const SCHEMA_FILE = join(SCHEMA_DIR, "codex-review-schema.json");
let schemaMaterialized = false;

Expand Down
4 changes: 2 additions & 2 deletions packages/server/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* so users can discover and reopen closed browser tabs.
*/

import { homedir } from "os";
import { join } from "path";
import {
mkdirSync,
Expand All @@ -15,6 +14,7 @@ import {
unlinkSync,
existsSync,
} from "fs";
import { getPlannotatorDataDir } from "@plannotator/shared/data-dir";

export interface SessionInfo {
pid: number;
Expand All @@ -27,7 +27,7 @@ export interface SessionInfo {
}

function getSessionsDir(): string {
const dir = join(homedir(), ".plannotator", "sessions");
const dir = join(getPlannotatorDataDir(), "sessions");
mkdirSync(dir, { recursive: true });
return dir;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/server/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,18 @@ describe("getPlanDir", () => {

test("uses default when no custom path", () => {
const result = getPlanDir();
expect(result).toMatch(/\.plannotator\/plans$/);
expect(result).toMatch(/plans$/);
expect(result).toBe(getPlanDir(null));
});

test("uses default for null", () => {
const result = getPlanDir(null);
expect(result).toMatch(/\.plannotator\/plans$/);
expect(result).toMatch(/plans$/);
});

test("uses default for whitespace-only custom path", () => {
const result = getPlanDir(" ");
expect(result).toMatch(/\.plannotator\/plans$/);
expect(result).toMatch(/plans$/);
expect(result).not.toBe(process.cwd());
});
});
Expand Down
5 changes: 3 additions & 2 deletions packages/server/tour/tour-review.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { join } from "node:path";
import { homedir, tmpdir } from "node:os";
import { tmpdir } from "node:os";
import { mkdir, writeFile, readFile, unlink } from "node:fs/promises";
import { getPlannotatorDataDir } from "@plannotator/shared/data-dir";
import type { DiffType } from "../vcs";
import type { PRMetadata } from "../pr";
import { getLocalDiffInstruction } from "../agent-review-message";
Expand Down Expand Up @@ -383,7 +384,7 @@ export function buildTourClaudeCommand(prompt: string, model: string = "sonnet",
};
}

const TOUR_SCHEMA_DIR = join(homedir(), ".plannotator");
const TOUR_SCHEMA_DIR = getPlannotatorDataDir();
const TOUR_SCHEMA_FILE = join(TOUR_SCHEMA_DIR, "tour-schema.json");
let tourSchemaMaterialized = false;

Expand Down
4 changes: 2 additions & 2 deletions packages/shared/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* Runtime-agnostic: uses only node:fs, node:os, node:child_process.
*/

import { homedir } from "os";
import { join } from "path";
import { getPlannotatorDataDir } from "./data-dir";
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
import { execSync } from "child_process";

Expand Down Expand Up @@ -127,7 +127,7 @@ export interface PlannotatorConfig {
pfmReminder?: boolean;
}

const CONFIG_DIR = join(homedir(), ".plannotator");
const CONFIG_DIR = getPlannotatorDataDir();
const CONFIG_PATH = join(CONFIG_DIR, "config.json");

/**
Expand Down
42 changes: 42 additions & 0 deletions packages/shared/data-dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Plannotator Data Directory
*
* Returns the base directory for all Plannotator data files.
*
* Priority:
* 1. PLANNOTATOR_DATA_DIR environment variable (with ~ expansion)
* 2. Default: ~/.plannotator
*
* This mirrors PASTE_DATA_DIR for the paste service and allows users
* to relocate all data (plans, history, drafts, config, hooks, sessions,
* debug logs, IPC registry, etc.) via a single variable — useful for
* XDG-style home directory cleanliness on Unix systems.
*/

import { homedir } from "os";
import { join, resolve } from "path";

/**
* Resolve the Plannotator data directory.
*
* If PLANNOTATOR_DATA_DIR is set and non-empty, the value is used
* as the base directory. Leading ~ is expanded to the user's home
* directory.
*
* Falls back to ~/.plannotator when the env var is absent or empty.
*/
export function getPlannotatorDataDir(): string {
const envDir = process.env.PLANNOTATOR_DATA_DIR?.trim();
if (!envDir) {
return join(homedir(), ".plannotator");
}

// Expand ~ to home directory
const home = homedir();
if (envDir === "~") return home;
if (envDir.startsWith("~/") || envDir.startsWith("~\\")) {
return join(home, envDir.slice(2));
}

return resolve(envDir);
}
4 changes: 2 additions & 2 deletions packages/shared/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
* Runtime-agnostic: uses only node:fs, node:path, node:os, node:crypto.
*/

import { homedir } from "os";
import { join } from "path";
import { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync } from "fs";
import { createHash } from "crypto";
import { getPlannotatorDataDir } from "./data-dir";

/**
* Get the drafts directory, creating it if needed.
*/
export function getDraftDir(): string {
const dir = join(homedir(), ".plannotator", "drafts");
const dir = join(getPlannotatorDataDir(), "drafts");
mkdirSync(dir, { recursive: true });
return dir;
}
Expand Down
12 changes: 7 additions & 5 deletions packages/shared/improvement-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@
* - Same trust model as ~/.plannotator/config.json
*/

import { homedir } from "os";
import { join } from "path";
import { readFileSync, statSync } from "fs";
import { getPlannotatorDataDir } from "./data-dir";

/** Base directory for hook-injectable files (new path) */
const HOOKS_BASE_DIR = join(homedir(), ".plannotator", "hooks");
const DATA_DIR = getPlannotatorDataDir();

/** Legacy base directory (pre-migration path) */
const LEGACY_BASE_DIR = join(homedir(), ".plannotator");
/** Hooks subdirectory (preferred location) */
const HOOKS_BASE_DIR = join(DATA_DIR, "hooks");

/** Fallback: hooks placed directly in the data dir (pre-hooks-subdir layout) */
const LEGACY_BASE_DIR = DATA_DIR;

/** Maximum file size to read (50 KB) */
const MAX_FILE_SIZE = 50 * 1024;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"./external-annotation": "./external-annotation.ts",
"./agent-jobs": "./agent-jobs.ts",
"./config": "./config.ts",
"./data-dir": "./data-dir.ts",
"./prompts": "./prompts.ts",
"./improvement-hooks": "./improvement-hooks.ts",
"./pfm-reminder": "./pfm-reminder.ts",
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/pr-gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
* Self-hosted instances are supported via the --hostname flag.
*/

import { homedir } from "os";
import { join } from "path";
import { mkdirSync, writeFileSync } from "fs";
import type { PRRuntime, PRMetadata, PRContext, PRReviewFileComment, CommandResult } from "./pr-types";
import { encodeApiFilePath } from "./pr-types";
import { getPlannotatorDataDir } from "./data-dir";

// GitLab-specific MRRef shape (used internally)
interface GlMRRef {
Expand Down Expand Up @@ -562,7 +562,7 @@ export async function submitGlMRReview(
.filter((c): c is PRReviewFileComment => c !== null);
let savedTo: string | null = null;
try {
const dir = join(homedir(), ".plannotator", "failed-comments");
const dir = join(getPlannotatorDataDir(), "failed-comments");
mkdirSync(dir, { recursive: true });
const slug = `${ref.host}-${ref.projectPath.replace(/\//g, "_")}-mr${ref.iid}-${Date.now()}`;
savedTo = join(dir, `${slug}.json`);
Expand Down
Loading