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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions harnessIntegrations/claude/skills/planbridge-last/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
name: planbridge-last
description: Open your most recent assistant message in the PlanBridge browser UI for human annotation. Use when the user wants to annotate or give feedback on the wall of text you just wrote.
---

# Annotate your most recent message in PlanBridge

Opens your most recent assistant message in the PlanBridge browser UI, where the user can leave inline annotations and general comments. Their feedback is returned on stdout.

## When to use

Use this skill when the user wants to mark up something you just said. Typical triggers:

- "Let me annotate that"
- "Open the last thing you said in PlanBridge"
- "I want to mark up what you just wrote"
- You just produced a long block of prose, such as a spec, architecture explanation, migration plan, code-review summary, draft commit message, or brainstorming section, and the user wants to give targeted feedback.

## How to invoke

**Do not send any commentary or status message before running the command.** The command targets your most recent rendered message, so a preamble like "Sure, opening that now" can mistakenly become the thing being annotated. Run the command first; speak after.

Copy the immediately previous assistant response from the conversation context and pipe it verbatim into `contextbridge open` via stdin. Preserve all markdown formatting and code blocks. Do not paraphrase, summarize, or rewrite. The user wants to annotate the exact text you produced, not a cleaned-up version.

```sh
contextbridge open <<'PLANBRIDGE_LAST_MESSAGE'
<your immediately previous assistant response, verbatim>
PLANBRIDGE_LAST_MESSAGE
```

Run the command yourself rather than telling the user to invoke shell syntax manually.

If the previous assistant response is no longer available in conversation context, say so and ask the user to provide the content or use `planbridge-open` for a file or specific document.

If `contextbridge` is not available on PATH, report that the PlanBridge CLI is unavailable in the current user environment.

## What happens

1. PlanBridge starts a local browser session and prints the URL.
2. The user annotates in the browser. Block on the CLI until they submit.
3. The CLI prints a markdown summary of the user's feedback to stdout.

## What to do with the output

Treat the comments the way you would treat a colleague's review notes: context for the next step, not a checklist to silently execute.

- If the user left no annotations, acknowledge briefly and continue.
- If the user left annotations, respond conversationally. They may want edits, may want discussion, or may be flagging things for later. When in doubt, ask what they want to do next.

## Limitations

Only your immediately previous assistant response is opened. To annotate a file on disk, an earlier message, or a specific section that is not your prior message, use `planbridge-open` instead.

Very long messages, around 5k+ words, may drift or truncate during verbatim reproduction. For specs and other content of that size, save the content to a file first and use `planbridge-open` against the path.
58 changes: 58 additions & 0 deletions harnessIntegrations/codex/skills/planbridge-last/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
name: planbridge-last
description: Open your most recent assistant message in the PlanBridge browser UI for human annotation. Use when the user wants to annotate or give feedback on the wall of text you just wrote.
---

# Annotate your most recent message in PlanBridge

Opens your most recent assistant message in the PlanBridge browser UI, where the user can leave inline annotations and general comments. Their feedback is returned on stdout.

## When to use

Use this skill when the user wants to mark up something you just said. Typical triggers:

- "Let me annotate that"
- "Open the last thing you said in PlanBridge"
- "I want to mark up what you just wrote"
- You just produced a long block of prose, such as a spec, architecture explanation, migration plan, code-review summary, draft commit message, or brainstorming section, and the user wants to give targeted feedback.

## How to invoke

**Do not send any commentary or status message before running the command.** The command targets your most recent rendered message, so a preamble like "Sure, opening that now" can mistakenly become the thing being annotated. Run the command first; speak after.

Copy the immediately previous assistant response from the conversation context and pipe it verbatim into `contextbridge open` via stdin. Preserve all markdown formatting and code blocks. Do not paraphrase, summarize, or rewrite. The user wants to annotate the exact text you produced, not a cleaned-up version.

```sh
contextbridge open <<'PLANBRIDGE_LAST_MESSAGE'
<your immediately previous assistant response, verbatim>
PLANBRIDGE_LAST_MESSAGE
```

## Running this from Codex

In sandboxed Codex environments, this skill usually requires resources outside the default sandbox. When invoking the command, set `sandbox_permissions: "require_escalated"` and supply a brief `justification` describing what the command does. If the user runs the command regularly, suggest a matching `prefix_rule` (e.g., `["contextbridge", "open"]`) so future invocations don't require re-approval.

Run the command yourself rather than telling the user to invoke shell syntax manually.

If the previous assistant response is no longer available in conversation context, say so and ask the user to provide the content or use `planbridge-open` for a file or specific document.

If `contextbridge` is not available on PATH, report that the PlanBridge CLI is unavailable in the current user environment.

## What happens

1. PlanBridge starts a local browser session and prints the URL.
2. The user annotates in the browser. Block on the CLI until they submit.
3. The CLI prints a markdown summary of the user's feedback to stdout.

## What to do with the output

Treat the comments the way you would treat a colleague's review notes: context for the next step, not a checklist to silently execute.

- If the user left no annotations, acknowledge briefly and continue.
- If the user left annotations, respond conversationally. They may want edits, may want discussion, or may be flagging things for later. When in doubt, ask what they want to do next.

## Limitations

Only your immediately previous assistant response is opened. To annotate a file on disk, an earlier message, or a specific section that is not your prior message, use `planbridge-open` instead.

Very long messages, around 5k+ words, may drift or truncate during verbatim reproduction. For specs and other content of that size, save the content to a file first and use `planbridge-open` against the path.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The CLI takes a file path or stdin content:

## Running this from Codex

This skill runs commands that require resources outside Codex's default sandbox. When invoking them, set `sandbox_permissions: "require_escalated"` and supply a brief `justification` describing what the command does. If the user runs the command regularly, suggest a matching `prefix_rule` (e.g., `["contextbridge", "<subcommand>"]`) so future invocations don't require re-approval.
In sandboxed Codex environments, this skill usually requires resources outside the default sandbox. When invoking the command, set `sandbox_permissions: "require_escalated"` and supply a brief `justification` describing what the command does. If the user runs the command regularly, suggest a matching `prefix_rule` (e.g., `["contextbridge", "open"]`) so future invocations don't require re-approval.

### Resolving the argument

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ stdin / [path] ─▶ contextbridge plan
{ status: "approved" | "changes_requested", annotations: [...] }
```

The loop is kind-agnostic: it serves a markdown document into the annotation UI and emits a structured result on stdout. Sibling entrypoints on this same loop today: `plan` (plan-mode handoffs from Claude Code and Codex hooks) and `open` (manual annotation of a markdown file or piped content, surfaced as `/planbridge-open` or `/planbridge:planbridge-open` in Claude and `$planbridge-open` in Codex). The CLI process owns the server and the browser lifecycle. When the user submits in the browser, the server shuts down and the CLI emits its structured result to stdout. That stdout contract is what harness hooks (Claude `exitPlanMode`, Codex Stop hook, etc.) consume.
The loop is kind-agnostic: it serves a markdown document into the annotation UI and emits a structured result on stdout. Sibling entrypoints on this same loop today: `plan` (plan-mode handoffs from Claude Code and Codex hooks) and `open` (manual annotation of a markdown file or piped content, surfaced through `planbridge-open` and `planbridge-last` skills in Claude and Codex). The CLI process owns the server and the browser lifecycle. When the user submits in the browser, the server shuts down and the CLI emits its structured result to stdout. That stdout contract is what harness hooks (Claude `exitPlanMode`, Codex Stop hook, etc.) consume.

## Build: embedded annotation UI

Expand Down
55 changes: 33 additions & 22 deletions packages/cli/src/installers/CodexInstaller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { createStubContext, readErrorLogs } from '#src/testHelpers/index.ts';
import { CodexInstaller } from './CodexInstaller.ts';

const CODEX_BINARY = getHarness('codex').binaryName;
const bundledOpenSkill = bundledSkills.find((skill) => skill.installId === 'planbridge-open')?.body ?? '';

describe('CodexInstaller', () => {
let tmp: string;
Expand Down Expand Up @@ -149,25 +148,21 @@ describe('CodexInstaller', () => {
expect(installPromise).rejects.toThrow(/^nope$/);
});

it('writes the skill to ~/.agents/skills/planbridge-open/SKILL.md on user-scope install', async () => {
it('writes bundled skills to ~/.agents/skills on user-scope install', async () => {
const { installer, context } = createCodexInstallerContext(tmp);

await installer.install(context, { yes: true });

const skillPath = join(tmp, '.agents', 'skills', 'planbridge-open', 'SKILL.md');
expect(existsSync(skillPath)).toBe(true);
expect(readFileSync(skillPath, 'utf8')).toBe(bundledOpenSkill);
expectBundledSkillsInstalled(tmp);
});

it('writes the skill at user level even on project-scope install', async () => {
it('writes bundled skills at user level even on project-scope install', async () => {
const project = join(tmp, 'project');
const { installer, context } = createCodexInstallerContext(tmp, { projectRoot: project });

await installer.install(context, { yes: true, scope: 'project' });

const skillPath = join(tmp, '.agents', 'skills', 'planbridge-open', 'SKILL.md');
expect(existsSync(skillPath)).toBe(true);
expect(readFileSync(skillPath, 'utf8')).toBe(bundledOpenSkill);
expectBundledSkillsInstalled(tmp);
});

it('skill install is idempotent', async () => {
Expand All @@ -176,8 +171,7 @@ describe('CodexInstaller', () => {
await installer.install(context, { yes: true });
await installer.install(context, { yes: true });

const skillPath = join(tmp, '.agents', 'skills', 'planbridge-open', 'SKILL.md');
expect(readFileSync(skillPath, 'utf8')).toBe(bundledOpenSkill);
expectBundledSkillsInstalled(tmp);
});
});

Expand Down Expand Up @@ -219,7 +213,7 @@ describe('CodexInstaller', () => {
expect(readFileSync(hooksPath, 'utf8')).toBe('{"hooks":[]}');
});

it('user-scope uninstall removes planbridge-open/ but leaves sibling skill dirs intact', async () => {
it('user-scope uninstall removes bundled skills but leaves sibling skill dirs intact', async () => {
const skillsDir = join(tmp, '.agents', 'skills');
await mkdir(join(skillsDir, 'some-other-tool'), { recursive: true });
await writeFile(join(skillsDir, 'some-other-tool', 'SKILL.md'), 'other tool skill');
Expand All @@ -228,7 +222,7 @@ describe('CodexInstaller', () => {

await installer.uninstall(context, { yes: true });

expect(existsSync(join(skillsDir, 'planbridge-open'))).toBe(false);
expectBundledSkillsAbsent(tmp);
expect(readFileSync(join(skillsDir, 'some-other-tool', 'SKILL.md'), 'utf8')).toBe('other tool skill');
});

Expand All @@ -239,8 +233,7 @@ describe('CodexInstaller', () => {

await installer.uninstall(context, { yes: true, scope: 'project' });

const skillPath = join(tmp, '.agents', 'skills', 'planbridge-open', 'SKILL.md');
expect(existsSync(skillPath)).toBe(true);
expectBundledSkillsInstalled(tmp);
});

it('user-scope uninstall is idempotent when skill is already gone', async () => {
Expand All @@ -265,7 +258,7 @@ describe('CodexInstaller', () => {
});
});

it('reports an installed hook and skill when both are present', async () => {
it('reports an installed hook and bundled skills when both are present', async () => {
const { installer, context } = createCodexInstallerContext(tmp);
await installer.install(context, { yes: true });

Expand All @@ -274,11 +267,9 @@ describe('CodexInstaller', () => {
descriptor: { id: 'codex' },
detected: true,
installed: true,
managed: [
{ kind: 'hook', identifier: 'contextbridge hook codex', scope: 'user' },
{ kind: 'skill', identifier: 'planbridge-open', scope: 'user' },
],
});
expect(status.managed).toContainEqual({ kind: 'hook', identifier: 'contextbridge hook codex', scope: 'user' });
expectManagedBundledSkills(status.managed);
});

it('reports hook-only state as installed because install owns feature setup', async () => {
Expand Down Expand Up @@ -330,13 +321,13 @@ describe('CodexInstaller', () => {
expect(installer.status(context)).rejects.toBeInstanceOf(CommanderError);
});

it('reports the skill as a ManagedEntry when installed', async () => {
it('reports bundled skills as ManagedEntries when installed', async () => {
const { installer, context } = createCodexInstallerContext(tmp);
await installer.install(context, { yes: true });

const status = await installer.status(context);

expect(status.managed).toContainEqual({ kind: 'skill', identifier: 'planbridge-open', scope: 'user' });
expectManagedBundledSkills(status.managed);
expect(status.installed).toBe(true);
});

Expand Down Expand Up @@ -396,6 +387,26 @@ function writeHooksJson(path: string, value: unknown): void {
writeFileSync(path, JSON.stringify(value));
}

function expectBundledSkillsInstalled(home: string): void {
for (const { installId, body } of bundledSkills) {
const skillPath = join(home, '.agents', 'skills', installId, 'SKILL.md');
expect(existsSync(skillPath)).toBe(true);
expect(readFileSync(skillPath, 'utf8')).toBe(body);
}
}

function expectBundledSkillsAbsent(home: string): void {
for (const { installId } of bundledSkills) {
expect(existsSync(join(home, '.agents', 'skills', installId))).toBe(false);
}
}

function expectManagedBundledSkills(managed: readonly unknown[]): void {
for (const { installId } of bundledSkills) {
expect(managed).toContainEqual({ kind: 'skill', identifier: installId, scope: 'user' });
}
}

function commandRunnerCalls(context: ReturnType<typeof createStubContext>['context'], args: readonly string[]) {
const commandRunner = context.commandRunner as unknown as {
callsTo(cmd: string, args: readonly string[]): readonly unknown[];
Expand Down
2 changes: 1 addition & 1 deletion packages/skills/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Partials emit content directly. Wrap them in a `{{#if (eq harness.id "…")}}` b
4. `bun run skills:check && bun run --cwd packages/cli test`.
5. Commit `sources/`, every `harnessIntegrations/<id>/skills/...`, and the `codex.ts` change together.

For the manual open skill, Claude may expose `/planbridge-open` or `/planbridge:planbridge-open`; Codex exposes `$planbridge-open`.
For manual skills, Claude may expose `/planbridge-open` or `/planbridge:planbridge-open` (and similarly for `/planbridge-last`); Codex exposes `$planbridge-open` and `$planbridge-last`.

## Adding a new harness

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## Running this from Codex

This skill runs commands that require resources outside Codex's default sandbox. When invoking them, set `sandbox_permissions: "require_escalated"` and supply a brief `justification` describing what the command does. If the user runs the command regularly, suggest a matching `prefix_rule` (e.g., `["contextbridge", "<subcommand>"]`) so future invocations don't require re-approval.
In sandboxed Codex environments, this skill usually requires resources outside the default sandbox. When invoking the command, set `sandbox_permissions: "require_escalated"` and supply a brief `justification` describing what the command does. If the user runs the command regularly, suggest a matching `prefix_rule` (e.g., `["contextbridge", "open"]`) so future invocations don't require re-approval.
58 changes: 58 additions & 0 deletions packages/skills/sources/planbridge-last/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback welcome on the phrasing here!

name: planbridge-last
description: Open your most recent assistant message in the PlanBridge browser UI for human annotation. Use when the user wants to annotate or give feedback on the wall of text you just wrote.
---

# Annotate your most recent message in PlanBridge

Opens your most recent assistant message in the PlanBridge browser UI, where the user can leave inline annotations and general comments. Their feedback is returned on stdout.

## When to use

Use this skill when the user wants to mark up something you just said. Typical triggers:

- "Let me annotate that"
- "Open the last thing you said in PlanBridge"
- "I want to mark up what you just wrote"
- You just produced a long block of prose, such as a spec, architecture explanation, migration plan, code-review summary, draft commit message, or brainstorming section, and the user wants to give targeted feedback.

## How to invoke

**Do not send any commentary or status message before running the command.** The command targets your most recent rendered message, so a preamble like "Sure, opening that now" can mistakenly become the thing being annotated. Run the command first; speak after.

Copy the immediately previous assistant response from the conversation context and pipe it verbatim into `contextbridge open` via stdin. Preserve all markdown formatting and code blocks. Do not paraphrase, summarize, or rewrite. The user wants to annotate the exact text you produced, not a cleaned-up version.

```sh
contextbridge open <<'PLANBRIDGE_LAST_MESSAGE'
<your immediately previous assistant response, verbatim>
PLANBRIDGE_LAST_MESSAGE
```

{{#if (eq harness.id "codex")}}
{{> codex/sandbox-escalation}}
{{/if}}

Run the command yourself rather than telling the user to invoke shell syntax manually.

If the previous assistant response is no longer available in conversation context, say so and ask the user to provide the content or use `planbridge-open` for a file or specific document.

If `contextbridge` is not available on PATH, report that the PlanBridge CLI is unavailable in the current user environment.

## What happens

1. PlanBridge starts a local browser session and prints the URL.
2. The user annotates in the browser. Block on the CLI until they submit.
3. The CLI prints a markdown summary of the user's feedback to stdout.

## What to do with the output

Treat the comments the way you would treat a colleague's review notes: context for the next step, not a checklist to silently execute.

- If the user left no annotations, acknowledge briefly and continue.
- If the user left annotations, respond conversationally. They may want edits, may want discussion, or may be flagging things for later. When in doubt, ask what they want to do next.

## Limitations

Only your immediately previous assistant response is opened. To annotate a file on disk, an earlier message, or a specific section that is not your prior message, use `planbridge-open` instead.

Very long messages, around 5k+ words, may drift or truncate during verbatim reproduction. For specs and other content of that size, save the content to a file first and use `planbridge-open` against the path.
Loading