Profiles

A profile is a saved, named set of agents and harnesses you can relaunch as one unit — a working setup for a recurring job, recreated with a single command:
profile launch writing-code
profile list
profile validate writing-codeprofile launch <name> opens a tab for every entry in the profile. Each one starts fresh, from the entry's saved name, working directory, and tab presentation: an agent tab does not come back with the transcript, command history, or schedule it had when it was saved, and a harness launches with whatever model, directory, workspace flag, and startup commands the profile specifies. Each entry joins the group its own group key names; any entry that names none instead lands in one shared new group, so a profile with no authored groups reads as its own colored band in the strip. profile list names the profiles you have; a name that doesn't exist gets No profile named "<name>".

Picking a profile to launch
Bare profile launch, with no name, opens a picker with a Project section followed by a Janissary section for the profiles bundled with the app. ↑/↓ move the selection and skip the section labels. Enter or a click fills the command line with profile launch <name> without submitting it, so you can review or edit it first. Escape closes the picker without picking anything.
The bundled ones are debugging, features, multitasking, planning, and product-review. Launch any of them by name without setting anything up, and read one as a worked example of the format below. profile list shows them alongside your own.
Writing a profile

Your profiles live in the profiles/ directory in your project: plain files meant to be committed and shared. Janissary also includes built-in profiles. If both sources use the same profile name, your project copy wins. Saving always writes to your project, so you can customize a built-in profile by saving a same-named replacement. Each profile is a single JSON file (dasherized, like writing-code.json) with one tabs array. Every element names its kind with a type, and an agent or harness entry also carries its own name, which becomes the tab's label:
{
"tabs": [
{ "type": "agent", "name": "planner", "number": 1 },
{
"type": "harness",
"name": "builder",
"tool": "opencode",
"model": "openai/gpt-5",
"workspace": true,
"run": ["review the open pull requests"],
"schedule": ["tests every 2h npm test"],
"number": 2,
"pane": "right"
}
]
}The eleven types are agent, harness, editor, files, notifications, schedules, plugin, image, markdown, page, and ssh. An agent entry uses the same format as saved agent state — just a name is a valid start. A harness entry names which binary to launch with a tool field (claude, opencode, or codex) — type already means the kind of tab — and supports a few more fields:
model— passed to the harness verbatim; an unknown model for that harness is reported and the entry skipped.effort— an effort/thinking level, forwarded verbatim like--efforton the interactiveharnesscommand (translated to each harness's own flag: claude--effort, codex-c model_reasoning_effort, opencode has none). Not validated against any fixed set of levels.workspace— launch in a fresh workspace clone. It defaults totrue; set it tofalseto opt out.autoApprove— auto-approve permission prompts. It defaults totruefor claude and codex andfalsefor opencode; explicitly setting it totruefor opencode reports an unsupported setting and skips that entry.offline: deny network access inside the workspace sandbox. It is only meaningful whenworkspaceis enabled.remote: launch on another machine, using the address you would put afteronin a harness command, such as"dev@example.com:project". It impliesworkspace, omitscwd, and asks for authentication once for each remote tab the profile opens.cwd— starting directory.$rootresolves to the project's launch directory and~to home, so you can write a portable path instead of an absolute one — aprofile saved entry captures itscwdthis way automatically when it's under the project root.run— commands typed into the harness once, shortly after launch.schedule— timers in theschedulegrammar, minus the leadingschedulekeyword and anyin <tab>clause (each line belongs to this tab). A line that doesn't parse is reported at launch and skipped.
An agent entry may use the same remote field. It reconnects that agent on the named host but restores no transcript, history, or context. A remote agent or harness always gets a fresh workspace on its host.
Every entry that takes a place in the tab strip carries its presentation as plain keys alongside type: color (the dot color), number (tab order), group, groupColor, pane, and focus. Set pane to left or right to reopen that entry in a two-pane center layout; omitting it means left. Any main-area entry with focus: true can claim keyboard focus after launch; the lowest-numbered focused entry wins, while the other pane keeps one of its own tabs visible. Without one, the first newly opened profile tab stays active.
Two profile-level keys sit outside tabs, because neither is a tab: monitors and layout. A monitor entry names its persona and targets:
{
"monitors": [
{ "name": "review-watch", "persona": "reviewer", "targets": ["group:2", "builder"] },
{ "persona": "triage", "targets": [] }
]
}persona is required. targets is a list of tab labels or group:<number> targets from the monitor command; an empty list creates an inline monitor. name is optional and defaults to persona. Monitors start after every profile tab is open, owned by the tab that issued profile launch, so closing the profile tabs does not stop them. Relaunching first stops an existing monitor with the same owner and name, then starts its replacement. The launch report includes the result for each monitor.
The layout key groups the sidebar widths under a nested sidebar object, e.g. "layout": { "sidebar": { "left": 300, "right": 280 }, "tabAreaPct": 75, "window": { "width": 1280, "height": 800 } }. It applies on every launch, including a relaunch, and always wins over anything you resized by hand: any dimension the key doesn't mention resets to the app's own default rather than staying at whatever it currently is. When Janissary starts with --no-open, the window size is ignored, while the sidebar widths and tab-area split still apply to a connected browser.
Use an editor entry to open a file directly in the in-app editor when the profile launches:
{ "type": "editor", "path": "$root/product/backlog/features.md", "line": 1, "number": 3 }Each has a required path, optional resolving tab in, and optional cursor line. $root resolves from the launch directory and ~ from home; another relative path resolves from in or the first newly opened profile tab. A missing file opens an empty buffer and is created only on save. Relaunching reuses an already-open editor tab for the same file and moves its cursor to the requested line.
For a file covered by GitHub syncing, author the original project-relative source path, not its temporary path inside the sync workspace. profile save records that source path automatically, so launching the profile provisions or reuses the shared workspace and opens the real content. If the editor refuses a path, for example because the file exceeds its size limit, that entry is skipped without moving or focusing any other tab.
The plugin, page, and ssh types reopen the rest of a working session — a diagram or video, a spec you keep reading, a docs site, a remote box:
{ "type": "plugin", "id": "image", "path": "$root/docs/architecture.png", "number": 4 },
{ "type": "page", "url": "https://example.com/", "number": 5 },
{ "type": "ssh", "destination": "devbox", "options": ["-p", "2222"], "number": 6 }A plugin entry names the bundled plugin that owns the tab, and is what profile save writes for any plugin tab you have open. For one that opens on a file — image, markdown, video — it carries that file's path, and a launch reopens it by opening the file again. A plugin reached only by its own command, such as the schedules list, carries no path; a launch reissues the command instead. The older { "type": "image", "path": … } and { "type": "markdown", "path": … } spellings still launch exactly the same way, so profiles you saved before are unaffected.
None of these needs a name — the label is derived the same way typing open or ssh derives it. Relaunching closes a page or ssh tab already showing the same url or destination before reopening it, so you end up with one of each rather than a duplicate; an already-open image, markdown, or video tab is simply reused.
A notifications entry opens the singleton feed. Add dock to put it in a sidebar, and use focus: true with dock when the feed should be the visible docked tab even if the profile also docks another view on that side:
{ "type": "notifications", "dock": "right", "focus": true }A files entry opens a file navigator, and can bring back the state of the tree itself — which directories were open, and which rows were selected:
{
"type": "files",
"dock": "left",
"path": "$root",
"details": "modified",
"expanded": ["src", "src/file-navigator"],
"cursor": "src/file-navigator/manager.ts",
"selected": ["src/file-navigator/manager.ts"]
}Use in with a tab label to root the tree at that tab's working directory. With no in or path, the navigator uses the first new profile tab, or the issuing tab if nothing new opened. A literal path sets the root directly; $root always means the profile launch directory. details selects the row detail mode: name, size, modified, or permissions. Omit it for the default name mode.
Every saved view path is relative to the tree's root. Restoring is quiet and forgiving: a directory or row that no longer exists is dropped without a word, and a restored selection never steals keyboard focus. The profile's own focus still decides where you land. Leave off dock and the tree opens in the center strip instead, where it takes number, group, and pane like any other tab.
A harness entry's run and schedule live in memory only — closing the tab or quitting ends them. That's the point of the profile: the file is the source of truth, and every launch rebuilds the setup from it.
Relaunching

Launching a profile that's already running resets it: any open tab whose label matches a profile entry is closed first — processes killed, schedules dropped, workspaces removed — then everything opens fresh, with schedules re-based to now and new clones where asked. The only tab spared is the one you ran profile launch from; if the profile has an entry by that name, it's reported and skipped so the launch report has somewhere to land.
Saving the running session as a profile
profile save <name> captures your current session into profiles/<name>.json, the inverse of launching one. It writes <name> verbatim as the filename, with no dasherization, and captures every open tab, including the one you typed the command in. The one tab it always leaves out is the automatic root janus tab, since a relaunch always has its own fresh one to land in.
Each agent is captured as a clean template: its name, working directory, and tab presentation only. Command history, transcript, and any queued commands are deliberately left out, so launching the saved profile always starts that agent from scratch, not from where you left off. Each harness is captured the same way, plus its tool, model, effort, and workspace/offline/auto-approve flags; its scheduled and one-shot commands are never captured, since they only ever lived in memory. Whichever tab is currently active is saved with focus: true so a relaunch lands you back in the same place. Every captured main-area entry also saves pane as left or right, preserving which side of a split it occupied; the exact divider position is screen-local and resets to the middle.
Open images, markdown previews, videos, web pages, and SSH sessions are captured too — an SSH entry keeps the flags you connected with, so a relaunch reconnects the same way. Every file navigator is captured, docked or not, along with its tree view: which directories you had expanded, which row the cursor was on, and every row you had selected. Launching the profile puts the tree back the way you left it, quietly skipping anything that no longer exists. A navigator left in the center strip also remembers its group, order, and pane.
The window size, sidebar widths, and reporting-area split are captured into the profile's layout as they currently look, along with any running monitors. Under --no-open, the window size is omitted and the command reports that omission, while the sidebar and tab-area sizes are still captured. The only thing left out and named in the command's report is a monitor's own reporting tab.
Saving over an existing profile name atomically replaces it outright, with no confirmation prompt. Janissary keeps the previous file until the complete replacement is ready, so a capture or write failure leaves your last valid profile intact. The command reports what it captured: counts per tab type, plus monitors and docked tabs, followed by the list of anything skipped.
If several profile save commands arrive close together, they run in command order. Each finishes capturing file-navigator selections before the next starts, and a failed save does not block the saves behind it. If an asynchronous save or launch fails, the issuing transcript receives Profile command failed: <reason>. so the command does not appear unfinished.
Checking a profile without launching it
profile validate <name> checks a profile file's structure and reports either Profile "<name>" is valid. or every specific problem it finds, each naming the offending key, e.g. tabs[0]: tool must be a string. An entry with a missing or unrecognized type is reported the same way, listing the types it could have been. Bare profile validate, with no name, checks every profile in your project and reports each one's status in turn. This only checks the file's shape; problems that depend on your setup, an unknown model or harness, an unsupported autoApprove, are caught at launch time instead and reported per entry there.
profile launch on a structurally broken file opens nothing and reports Profile "<name>" is malformed., pointing you at profile validate <name> for the details.
A worked example
One profile that ties the pieces together — a morning code-review setup, all in profiles/morning-review.json:
{
"tabs": [
{ "type": "agent", "name": "triage", "number": 1 },
{
"type": "harness",
"name": "reviewer",
"tool": "claude",
"workspace": true,
"run": ["review the newest open PR and summarize the risky changes"],
"schedule": ["refresh every 2h check for newly opened PRs"],
"number": 2
}
]
}profile launch morning-review then: opens triage and reviewer as one new tab group; clones the repo into a workspace for reviewer (isolated from the rest of your machine); types the run prompt into the harness once it's up; and keeps nudging it every two hours via its schedule — visible in the schedule window floating over the harness. Close the tabs when you're done; tomorrow, one command rebuilds it all.