Plan
PRD authoring and plan execution lifecycle with tool restrictions during planning.
Overview
The plan layer manages a structured planning workflow: the agent enters a restricted "plan mode" where only read-only tools are allowed, writes a PRD document, optionally structures the plan as a JSON WorkflowDocument, then exits to execute the plan with full tool access.
A plan's tree IS a workflow document (JSON Workflow Runtime). Plans additionally store named workflows referenced from the tree via subflow nodes — the reviewed tree stays small while detailed mechanics live in separately-authored, separately-reviewable workflows.
- Slot:
240(Slot.PROCEDURAL - 10) - Scope:
thread(default) - Default budget:
{ min: 100, max: 3000 }
Usage
import { plan } from '@noetic-tools/core';
const layer = plan();The layer is included by default in the Noetic CLI. Users type /plan to enter plan mode.
Configuration
interface PlanConfig {
scope?: ContextScope;
additionalAllowedTools?: string[];
maxPrdLength?: number;
maxDepth?: number;
maxWorkflows?: number;
maxWorkflowChars?: number;
allowedNodeKinds?: WorkflowNode['kind'][];
style?: PlanStyle;
subAgentTool?: string;
additionalPlanInstructions?: string;
onEnterSession?: () => Promise<{ slug: string }>;
onExit?: (state: PlanState) => Promise<{ approved: boolean }>;
}| Field | Type | Default | Purpose |
|---|---|---|---|
scope | ContextScope | 'thread' | Persistence scope |
additionalAllowedTools | string[] | -- | Extra tools allowed during plan mode |
maxPrdLength | number | 50000 | Maximum PRD content length |
maxDepth | number | 5 | Maximum structural depth (workflowDepth) of the plan tree and each named workflow |
maxWorkflows | number | 20 | Maximum number of named workflows |
maxWorkflowChars | number | 20000 | Maximum serialized size of each workflow document |
allowedNodeKinds | WorkflowNode['kind'][] | all kinds | Optional profile restricting which node kinds a plan may use. Hosts that set this must include 'subflow' |
style | PlanStyle | 'phased' | How the planning briefing shapes the turn — see Planning styles |
subAgentTool | string | -- | Name of the host's sub-agent tool. Unset, the briefing omits all sub-agent guidance — see Sub-agents |
additionalPlanInstructions | string | -- | Extra free-form instructions appended to the planning-phase recall payload |
onEnterSession | () => Promise<{ slug: string }> | -- | Host callback invoked once on the Idle → Planning transition. Returns a session identifier the host owns (e.g. an on-disk plan directory slug), stored on the state as planSlug |
onExit | (state: PlanState) => Promise<{ approved: boolean }> | -- | Host callback invoked when the model requests plan/exitPlanMode with action: 'execute'. Return { approved: false } to keep the layer in Planning — the model is told the user rejected the plan and should revise it before requesting approval again |
Planning styles
A planning turn can be spent two ways, and which one fits depends on whether the missing information is in the codebase or in the user's head.
import { plan, PlanStyle } from '@noetic-tools/core';
const layer = plan({ style: PlanStyle.Interview });| Style | Shape of the turn | Reach for it when |
|---|---|---|
'phased' (default) | Understand → design → review → write → exit | The work's shape is known and the job is getting the details right |
'interview' | Explore → write → ask, on repeat. The PRD starts as a skeleton on the first turn and fills in as questions get answered. | Requirements are still vague and the user is the missing input |
Both styles enforce the same turn-ending rule: a turn ends with AskUserQuestion or with plan/exitPlanMode, never with a prose question. Asking "does this plan look right?" IS exitPlanMode — the briefing says so explicitly, because a model that asks in prose leaves the plan in limbo with no approval to act on.
Sub-agents
The layer ships no sub-agent tool. Set subAgentTool to the name of yours to add the parallel-exploration guidance to the briefing — how many explorers to launch (usually one, never more than three), when a second perspective is worth it, and what background to hand each agent so Phase 1's work is not thrown away at the Phase 2 boundary.
plan({ subAgentTool: 'agent' });Left unset, the briefing tells the model to explore directly. This is deliberate: instructing a model to call a tool that is not registered costs it a turn and teaches it nothing.
What the briefing says you may use
The "What you may use" section is rendered from the layer's own allow-set, so it always names exactly what beforeToolCall will let through — including anything added via additionalAllowedTools. The node-kind guidance is filtered by allowedNodeKinds the same way, and plan/setPlanTree's tool description is built from the same table, so the briefing and the tool cannot disagree about what a plan may contain.
Budget
The briefing and the plan state share the layer's budget ({ min: 100, max: 3000 } tokens by default). State gives way first: the largest of the PRD draft, the tree JSON, or the workflow summaries is trimmed to the remaining headroom rather than dropped whole, since a truncated draft is worth more than the blank space dropping it would leave. The model can read state back with plan/getWorkflow.
Rules are never cut mid-sentence. If they will not fit, a compact briefing replaces them — the restriction, where the plan goes, and how the turn ends — and if even that overflows, the layer contributes nothing that turn rather than half a rule.
Phase Lifecycle
The layer manages a state machine with five phases:
- idle — No active plan.
recallreturns nothing. - planning — Read-only tools only. LLM writes PRD, plan tree, and named workflows.
- executing — Full tool access. The plan is injected into the context of each model call.
- completed — Execution succeeded. Outcome recorded.
- failed — Execution failed or was aborted. Outcome recorded.
When onExit is configured, the Planning → Executing transition requires approval: if onExit returns { approved: false } (e.g. the user rejected the plan in the UI), the layer stays in planning and the model is prompted to address the feedback and call plan/exitPlanMode again.
Before onExit is ever invoked, the layer validates the plan structurally: every subflow ref (in the tree and inside stored workflows) must name a stored workflow, and named workflows must not reference each other in a cycle. The user is never asked to approve a plan that cannot hydrate.
LLM Tools
The layer exposes seven LLM tools and one data projection via its provides map:
| Tool | Description |
|---|---|
plan/enterPlanMode | Enter plan mode. Accepts optional goal string. Resets workflows from any prior plan. |
plan/updatePrd | Replace PRD content with new markdown. |
plan/setPlanTree | Set the plan as { document: WorkflowDocument }. Validates schema, depth, node kinds, and subflow-ref name syntax. Refs to not-yet-defined workflows are allowed — the result message enumerates them. |
plan/setWorkflow | Create or replace a named workflow ({ name, document }, upsert). Names are lowercase slugs, max 64 chars. |
plan/removeWorkflow | Delete a named workflow. Warns when the tree or another workflow still references it. |
plan/getWorkflow | Read back a stored workflow's JSON (recall shows summaries, not bodies). |
plan/exitPlanMode | Exit plan mode (execute or cancel). execute validates refs and cycles before requesting approval. |
status (data) | Read-only: { phase, hasPrd, hasPlanTree, workflowNames, version } |
Documents may be passed as objects or JSON strings (a common LLM tool-call shape); validation happens inside the tool with readable error messages, against the published schema at https://noetic.tools/schema/noetic-workflow.schema.json.
Tool Restrictions
During the planning phase, beforeToolCall restricts tool usage:
- Allowed:
Read,Grep,Find,Ls,AskUserQuestion,activateSkill, the sub-agent coordination tools (agent,checkAgent,sendMessage),requestPlanApproval, and allplan/*tools - Denied: All other tools (
Write,Edit,Bash, etc.)
Additional tools can be allowed via additionalAllowedTools in the config.
The Plan is a WorkflowDocument
plan/setPlanTree stores a WorkflowDocument — the same portable JSON format the JSON Workflow Runtime executes, with node kinds like callModel, invokeTool, sequence, inParallel, conditional, loop, spawn, subflow, and the coding-agent kinds (claude-code, codex, opencode, pi).
The planning-phase instructions steer the model to keep the tree small (~7 top-level nodes a reviewer can scan) and factor mechanics into named workflows:
{
"version": 1,
"root": {
"kind": "sequence",
"id": "plan",
"steps": [
{ "kind": "callModel", "id": "implement", "instructions": "Apply the PRD changes." },
{ "kind": "subflow", "id": "verify", "ref": "verify-loop" }
]
}
}with verify-loop defined separately via plan/setWorkflow. Named workflows may reference other named workflows, but cycles are rejected at exit.
Executing an Approved Plan
PlanState.planTree and PlanState.workflows map directly onto the workflow runtime:
const onExit: PlanExitCallback = async (state) => {
const approved = await ui.requestApproval(state.prd, state.planTree, state.workflows);
if (approved) {
void parseAndRunWorkflow({
json: state.planTree,
workflows: new Map(Object.entries(state.workflows)),
harness, ctx, tools, layers,
});
}
return { approved };
};The default CLI flow uses context injection instead — the plan is recalled into the LLM's view as an <active_plan> block and the model executes by making tool calls directly; parseAndRunWorkflow is the programmatic path.