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
10 changes: 10 additions & 0 deletions packages/junior-plugin-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,17 @@ export interface AgentPluginToolDefinition<TInput = unknown> {
executionMode?: unknown;
inputSchema: unknown;
prepareArguments?: (args: unknown) => unknown;
/**
* @deprecated Put tool-selection and usage guidance directly in `description`
* and parameter descriptions. Retained for compatibility; may be removed in a
* future major version.
*/
promptGuidelines?: string[];
/**
* @deprecated Put tool-selection and usage guidance directly in `description`
* and parameter descriptions. Retained for compatibility; may be removed in a
* future major version.
*/
promptSnippet?: string;
execute?: AgentPluginToolExecute<TInput>;
}
Expand Down
79 changes: 19 additions & 60 deletions packages/junior-scheduler/src/schedule-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,6 @@ export interface SchedulerToolContext {
const TASK_ID_PREFIX = "sched";
const MAX_LISTED_TASKS = 50;
const DEFAULT_SCHEDULE_TIMEZONE = "America/Los_Angeles";
const ACTIVE_DESTINATION_GUIDELINE =
"Only manage tasks for the active Slack DM or channel; never target an existing thread, another channel, or another user's DM.";
const ACTIVE_TASK_ID_GUIDELINE =
"Use only task IDs returned from this active destination.";
const RECURRING_GUIDELINE =
"Omit recurrence for one-time requests like 'in 1 minute', 'tomorrow', or a specific date; provide recurrence only for requests that explicitly repeat.";

const recurrenceInputSchema = Type.Union([
Type.Literal("daily"),
Type.Literal("weekly"),
Type.Literal("monthly"),
Type.Literal("yearly"),
]);

function throwToolInputError(error: string): never {
throw new AgentPluginToolInputError(error);
Expand Down Expand Up @@ -349,30 +336,24 @@ export function createSlackScheduleCreateTaskTool(
) {
return tool({
description:
"Create a scheduled Junior task in the active Slack conversation.",
promptSnippet: "create future or recurring Junior work here",
promptGuidelines: [
"Use only when the user explicitly asks Junior to do work later or on a recurring cadence.",
ACTIVE_DESTINATION_GUIDELINE,
RECURRING_GUIDELINE,
"When the user's scheduling intent is clear, create the task immediately without asking for confirmation.",
"Ask for confirmation only when the task contract, schedule, or active destination is ambiguous.",
"Recurring tasks can run at most once per day; use only daily, weekly, monthly, or yearly recurrence frequencies.",
"Provide next_run_at as an exact ISO timestamp computed from the user's requested schedule.",
"Provide recurrence only for repeating schedules.",
],
"Create a future or recurring Junior task in the active Slack conversation. Use only when the user explicitly asks Junior to do work later or on a recurring cadence. Only manage tasks for the active Slack DM or channel; never target threads, other channels, or another user's DM. When the task, schedule, and destination are clear, create it without asking for confirmation; ask only when one of those is ambiguous.",
inputSchema: Type.Object({
task: Type.String({ minLength: 1, maxLength: 4000 }),
schedule: Type.String({ minLength: 1, maxLength: 300 }),
timezone: Type.Optional(Type.String({ minLength: 1, maxLength: 80 })),
timezone: Type.Optional(Type.String({ minLength: 1, maxLength: 80, description: "IANA timezone, e.g. 'America/Los_Angeles'. Defaults to the channel's configured timezone." })),
next_run_at: Type.Optional(
Type.String({
minLength: 1,
description:
"Exact next run time as an ISO timestamp, computed from the user's requested schedule.",
}),
),
recurrence: Type.Optional(recurrenceInputSchema),
recurrence: Type.Optional(Type.Union([
Type.Literal("daily"),
Type.Literal("weekly"),
Type.Literal("monthly"),
Type.Literal("yearly"),
], { description: "Provide only for explicitly repeating schedules; omit for one-time requests like 'in 1 minute', 'tomorrow', or a specific date. Recurring tasks run at most once per day: use daily, weekly, monthly, or yearly only." })),
}),
execute: async (input) => {
const destination = requireActiveDestination(context);
Expand Down Expand Up @@ -438,12 +419,7 @@ export function createSlackScheduleListTasksTool(
) {
return tool({
description:
"List scheduled Junior tasks for the active Slack conversation.",
promptSnippet: "list schedules for this Slack destination",
promptGuidelines: [
"Use when the user asks what is scheduled here or needs task IDs before editing, deleting, or running schedules.",
ACTIVE_DESTINATION_GUIDELINE,
],
"List scheduled Junior tasks for the active Slack conversation. Use when the user asks what is scheduled here, or when task IDs are needed before editing, deleting, or running schedules. Only manages tasks for the active Slack DM or channel.",
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: Type.Object({}),
execute: async () => {
Expand Down Expand Up @@ -471,32 +447,23 @@ export function createSlackScheduleUpdateTaskTool(
context: SchedulerToolContext,
) {
return tool({
description: "Edit, pause, resume, or reschedule a Junior scheduled task.",
promptSnippet: "edit/pause/resume one schedule in this Slack destination",
promptGuidelines: [
ACTIVE_TASK_ID_GUIDELINE,
ACTIVE_DESTINATION_GUIDELINE,
RECURRING_GUIDELINE,
"Do not move scheduled tasks across conversations.",
"Provide next_run_at as an exact ISO timestamp when changing the next run.",
"Set recurrence to null when converting a recurring task to one-time.",
"Set status to active, paused, or blocked when the user asks to resume, pause, or block a task.",
],
description:
"Edit, pause, resume, or reschedule an existing Junior scheduled task in the active Slack conversation. Use only task IDs returned for this destination. Do not move scheduled tasks across conversations.",
inputSchema: Type.Object({
task_id: Type.String({ minLength: 1 }),
task_id: Type.String({ minLength: 1, description: "ID of the task to update. Must be from this active Slack destination." }),
task: Type.Optional(Type.String({ minLength: 1, maxLength: 4000 })),
schedule: Type.Optional(Type.String({ minLength: 1, maxLength: 300 })),
timezone: Type.Optional(Type.String({ minLength: 1, maxLength: 80 })),
next_run_at: Type.Optional(Type.String({ minLength: 1 })),
next_run_at: Type.Optional(Type.String({ minLength: 1, description: "Exact ISO timestamp when changing the next run time." })),
recurrence: Type.Optional(
Type.Union([recurrenceInputSchema, Type.Null()]),
Type.Union([Type.Literal("daily"), Type.Literal("weekly"), Type.Literal("monthly"), Type.Literal("yearly"), Type.Null()], { description: "Provide only for repeating schedules. Omit for one-time requests. Set to null to convert a recurring task to one-time." }),
),
status: Type.Optional(
Type.Union([
Type.Literal("active"),
Type.Literal("paused"),
Type.Literal("blocked"),
]),
], { description: "Set to active, paused, or blocked to resume, pause, or block the task." }),
),
}),
execute: async (input) => {
Expand Down Expand Up @@ -571,11 +538,9 @@ export function createSlackScheduleDeleteTaskTool(
) {
return tool({
description:
"Delete a Junior scheduled task from the active Slack conversation.",
promptSnippet: "delete one schedule from this Slack destination",
promptGuidelines: [ACTIVE_TASK_ID_GUIDELINE, ACTIVE_DESTINATION_GUIDELINE],
"Delete one scheduled Junior task from the active Slack conversation. Use only task IDs returned for this destination. Do not delete schedules from threads, other channels, or another user's DM.",
inputSchema: Type.Object({
task_id: Type.String({ minLength: 1 }),
task_id: Type.String({ minLength: 1, description: "ID of the task to delete. Must be from this active Slack destination." }),
}),
execute: async ({ task_id }) => {
const lookup = await getWritableTask({ context, taskId: task_id });
Expand Down Expand Up @@ -604,15 +569,9 @@ export function createSlackScheduleRunTaskNowTool(
) {
return tool({
description:
"Queue an active Junior scheduled task to run as soon as possible.",
promptSnippet: "run one active schedule now without changing its cadence",
promptGuidelines: [
ACTIVE_TASK_ID_GUIDELINE,
ACTIVE_DESTINATION_GUIDELINE,
"Use when the user asks to run an existing scheduled task now; do not rewrite the stored calendar cadence.",
],
"Queue an existing active scheduled Junior task to run as soon as possible, without changing its cadence. Use when the user asks to run an existing scheduled task now. Use only task IDs returned for this destination.",
inputSchema: Type.Object({
task_id: Type.String({ minLength: 1 }),
task_id: Type.String({ minLength: 1, description: "ID of the active task to run now. Must be from this active Slack destination." }),
}),
execute: async ({ task_id }) => {
const lookup = await getWritableTask({ context, taskId: task_id });
Expand Down
2 changes: 1 addition & 1 deletion packages/junior/src/chat/tools/advisor/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export interface AdvisorToolRuntimeContext {
}

const ADVISOR_TOOL_DESCRIPTION =
"Ask a stronger advisor for deep technical guidance. Call this when the task has a hard reasoning core: algorithm design, architecture, concurrency, security-sensitive logic, data modeling, unclear requirements, repeated failures, difficult debugging, broad refactors, or final review of nontrivial work. Pass a focused question plus curated context containing the exact evidence, constraints, current plan, alternatives, command output, code snippets, or diffs the advisor should start from. The advisor does not automatically receive the parent transcript, keeps its own advisor history for this parent conversation, can use read-only inspection tools to verify evidence, can reason deeply, and returns guidance for you to apply and verify. Follow-up calls can build on prior advisor guidance but must include any new evidence or changed constraints. Use it after initial orientation reads when repository context matters, before committing to a non-obvious implementation plan, when changing approach, when stuck, and before declaring complex work complete. Do not use it for greetings, simple deterministic edits, routine formatting, or tasks where the next action is already obvious from fresh tool output.";
"Use this before committing to a non-obvious technical plan or declaring complex work complete. Call proactively after initial orientation when repository context matters — especially for architecture, algorithms, data modeling, concurrency, security-sensitive logic, unclear requirements, broad refactors, difficult debugging, repeated failures, or high-risk changes. Provide a focused question plus curated context: exact evidence, constraints, relevant code snippets, command output, diffs, current plan, and alternatives considered. The advisor does not automatically receive the parent transcript, keeps its own advisor history for this parent conversation, and can use read-only inspection tools to verify evidence. Follow-up calls can build on prior advisor guidance but must include new evidence or changed constraints. Do not use for greetings, simple deterministic edits, routine formatting, or when the next action is obvious from fresh tool output.";

const ADVISOR_SYSTEM_PROMPT = [
"You are a senior technical advisor for the executor.",
Expand Down
10 changes: 10 additions & 0 deletions packages/junior/src/chat/tools/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ export interface ToolDefinition<TInputSchema extends TSchema = TSchema> {
description: string;
inputSchema: TInputSchema;
annotations?: ToolAnnotations;
/**
* @deprecated Put tool-selection and usage guidance directly in `description`
* and parameter descriptions. Retained for plugin compatibility; may be
* removed in a future major version.
*/
promptSnippet?: string;
/**
* @deprecated Put tool-selection and usage guidance directly in `description`
* and parameter descriptions. Retained for plugin compatibility; may be
* removed in a future major version.
*/
promptGuidelines?: string[];
prepareArguments?: (args: unknown) => Static<TInputSchema>;
executionMode?: ToolExecutionMode;
Expand Down
8 changes: 1 addition & 7 deletions packages/junior/src/chat/tools/sandbox/edit-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,7 @@ const editReplacementSchema = Type.Object(
export function createEditFileTool() {
return tool({
description:
"Edit one sandbox workspace file with exact text replacements. Use for precise changes to existing files. Each oldText must match exactly, be unique, and not overlap another edit. Returns a diff.",
promptSnippet: "existing-file exact edits; returns diff",
promptGuidelines: [
"prefer over writeFile for targeted changes",
"oldText exact, unique, non-overlapping",
"multiple same-file changes: one edits[] call",
],
"Edit one sandbox workspace file with exact text replacements. Use for precise changes to existing files; prefer this over writeFile for targeted changes. Each oldText must match exactly, be unique, and not overlap another edit. Returns a diff. Multiple changes to the same file: use one edits[] call.",
prepareArguments: prepareEditFileArguments,
executionMode: "sequential",
inputSchema: Type.Object(
Expand Down
4 changes: 1 addition & 3 deletions packages/junior/src/chat/tools/sandbox/write-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { tool } from "@/chat/tools/definition";
export function createWriteFileTool() {
return tool({
description:
"Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or replacement after validation. Do not use for exploratory analysis-only turns.",
promptSnippet: "new file or deliberate full-file replacement",
promptGuidelines: ["targeted existing-file changes: editFile"],
"Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or deliberate full-file replacement after validation; use editFile instead for targeted changes to existing files. Do not use for exploratory analysis-only turns.",
executionMode: "sequential",
inputSchema: Type.Object(
{
Expand Down
12 changes: 2 additions & 10 deletions packages/junior/src/chat/tools/slack/canvas-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,7 @@ export function createSlackCanvasReadTool() {
export function createSlackCanvasEditTool(state: ToolState) {
return tool({
description:
"Edit one Slack canvas with exact markdown replacements. Use for precise changes to existing Canvas content. Each oldText must match exactly, be unique, and not overlap another edit. Returns a diff.",
promptSnippet: "existing-canvas exact edits; returns diff",
promptGuidelines: [
"prefer over slackCanvasWrite for targeted changes",
"oldText exact, unique, non-overlapping",
"multiple same-canvas changes: one edits[] call",
],
"Edit one Slack canvas with exact markdown replacements. Use for precise changes to existing Canvas content; prefer this over slackCanvasWrite for targeted changes. Each oldText must match exactly, be unique, and not overlap another edit. Returns a diff. Multiple changes to the same canvas: use one edits[] call.",
prepareArguments: prepareCanvasEditArguments,
executionMode: "sequential",
inputSchema: Type.Object(
Expand Down Expand Up @@ -372,9 +366,7 @@ export function createSlackCanvasEditTool(state: ToolState) {
export function createSlackCanvasWriteTool(state: ToolState) {
return tool({
description:
"Write UTF-8 markdown content to a Slack canvas. Use for deliberate full-Canvas replacement after validation. Do not use for targeted edits.",
promptSnippet: "deliberate full-canvas replacement",
promptGuidelines: ["targeted existing-canvas changes: slackCanvasEdit"],
"Write UTF-8 markdown content to a Slack canvas. Use for deliberate full-Canvas replacement after validation; use slackCanvasEdit for targeted changes to existing canvas content.",
executionMode: "sequential",
inputSchema: Type.Object(
{
Expand Down
4 changes: 2 additions & 2 deletions specs/advisor-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Metadata

- Created: 2026-05-06
- Last Edited: 2026-05-28
- Last Edited: 2026-05-31

## Purpose

Expand Down Expand Up @@ -45,7 +45,7 @@ Input:
- `question`: required focused advisor question or decision point.
- `context`: required curated evidence packet with the requirements, constraints, current plan, alternatives, code snippets, diffs, command output, and open questions the advisor should start from.

The tool description is the executor-facing trigger policy. It must say the advisor is stronger, tool-backed, does not automatically receive the parent transcript, keeps advisor history for the parent conversation, receives a read-only tool subset, and is for hard reasoning rather than routine work.
The tool description is the executor-facing trigger policy. It must say the advisor should be called proactively before committing to a non-obvious plan or declaring complex work complete — not only when stuck. It must also say the advisor is stronger, tool-backed, does not automatically receive the parent transcript, keeps advisor history for the parent conversation, receives a read-only tool subset, and is for hard reasoning rather than routine work.

## Runtime Contract

Expand Down
Loading