Skip to content

Add swarm templates for predefined multi-agent workflows#19

Merged
2witstudios merged 5 commits into
mainfrom
ppg/templates
Feb 24, 2026
Merged

Add swarm templates for predefined multi-agent workflows#19
2witstudios merged 5 commits into
mainfrom
ppg/templates

Conversation

@2witstudios

@2witstudios 2witstudios commented Feb 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds ppg swarm <template> command that spawns multiple agents from YAML-defined swarm templates in .pg/swarms/
  • Supports shared (single worktree) and isolated (per-agent worktree) strategies, template variables via --var, and targeting existing worktrees with --worktree
  • Bundles a code-review swarm with quality, security, and regression review prompts — written during ppg init
  • Adds ppg list swarms subcommand and updates conductor skill references to prefer swarm templates

Changes since initial review

  • Input sanitization: Template name and YAML swarm name validated against SAFE_NAME regex to prevent path traversal
  • Null guard: YAML.parse null result handled before accessing properties (prevents TypeError on empty files)
  • Deterministic listing: listSwarms() sorts results alphabetically
  • JSON consistency: Empty swarms returns {"swarms":[]} when --json is passed
  • User var precedence: --var flags now override template-level agent vars
  • Incremental manifest writes: Worktree registered before spawning agents; each agent added incrementally to survive partial failures
  • Isolated branch dedup: Index suffix appended when isolated strategy has duplicate prompt names
  • List shows filename key: ppg list swarms displays the invokable filename, not the YAML name field
  • Path helper: init.ts uses promptFile() instead of inline path.join
  • Gitignore: .pg/swarms/ added to gitignore entries for consistency with .pg/prompts/

Test plan

  • npm test — 25 tests pass in swarm.test.ts (listSwarms, loadSwarm validation, name sanitization, empty YAML)
  • npm run typecheck — clean
  • CI green (Node 20 + 22)
  • ppg init in fresh repo — confirm .pg/swarms/code-review.yaml and .pg/prompts/review-*.md created
  • ppg list swarms — confirm code-review swarm listed with filename key
  • ppg swarm code-review --var CONTEXT="test" --no-open --json — verify agents spawn in shared worktree
  • ppg swarm code-review --worktree <existing-wt> --var CONTEXT="test" --no-open — verify agents attach to existing worktree

Introduce `ppg swarm <template>` command that spawns multiple agents from
YAML-defined swarm templates in `.pg/swarms/`. Supports shared (single worktree)
and isolated (per-agent worktree) strategies, template variables via `--var`,
and targeting existing worktrees with `--worktree`.

Includes bundled `code-review` swarm with quality, security, and regression
review prompts. Templates and prompts are written during `ppg init`.

Also adds `ppg list swarms` and updates conductor skill references.
@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds swarm template support: new CLI swarm, list support for swarms, bundled prompts and swarms, swarm core utilities (list/load/validation), a full swarm command implementation with multiple strategies, init changes to emit bundled assets, tests for swarm parsing/validation, and a new swarmsDir path helper.

Changes

Cohort / File(s) Summary
Documentation
skills/ppg-conductor/references/commands.md, skills/ppg-conductor/references/conductor.md, skills/ppg-conductor/references/modes.md
Adds ppg swarm docs and reorganizes multi-agent guidance into swarm templates, custom swarm mode, and updated spawn options. Note: commands.md contains a duplicated ppg swarm block.
Bundled Assets
src/bundled/prompts.ts, src/bundled/swarms.ts
Adds bundledPrompts (three code-review prompt templates) and bundledSwarms (a code-review swarm YAML string).
CLI
src/cli.ts
Introduces swarm command and extends list description to include swarms.
Commands: list & swarm
src/commands/list.ts, src/commands/swarm.ts
list now dispatches templates vs swarms and supports JSON/table outputs; new swarm command implements SwarmOptions and swarmCommand orchestration with shared/isolated/existing-worktree strategies, var parsing, prompt rendering, tmux integration, manifest updates, and JSON/human output.
Core swarm utilities & tests
src/core/swarm.ts, src/core/swarm.test.ts
Adds swarm models, listSwarms and loadSwarm (YAML loading + validation) and comprehensive tests covering parsing, validation rules, defaults, and error cases.
Init & paths
src/commands/init.ts, src/lib/paths.ts
Init now writes bundled prompts and swarm templates into .pg/prompts and .pg/swarms and updates .gitignore; adds swarmsDir() path helper.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI
    participant SwarmCmd as "swarmCommand"
    participant SwarmCore as "loadSwarms / listSwarms"
    participant Manifest
    participant Spawner
    participant TMUX
    participant FS

    User->>CLI: ppg swarm code-review --var KEY=value
    CLI->>SwarmCmd: invoke swarmCommand(template, options)
    SwarmCmd->>Manifest: load manifest
    SwarmCmd->>SwarmCore: loadSwarm(projectRoot, "code-review")
    SwarmCore->>FS: read swarm YAML
    FS-->>SwarmCore: YAML content
    SwarmCore-->>SwarmCmd: SwarmTemplate
    SwarmCmd->>SwarmCmd: choose strategy (shared / isolated / existing)
    alt Shared
        SwarmCmd->>FS: create worktree
        FS-->>SwarmCmd: worktree path
        SwarmCmd->>TMUX: create session/window
        loop per agent
            SwarmCmd->>FS: load prompt file
            FS-->>SwarmCmd: prompt content
            SwarmCmd->>Spawner: spawn agent (rendered prompt)
            Spawner->>TMUX: attach agent to window
            TMUX-->>Spawner: spawned
        end
    else Isolated
        loop per agent
            SwarmCmd->>FS: create worktree for agent
            SwarmCmd->>TMUX: create session/window
            SwarmCmd->>Spawner: spawn agent
        end
    else Existing
        SwarmCmd->>TMUX: ensure window/session
        loop new agents
            SwarmCmd->>Spawner: spawn agent into worktree
        end
    end
    SwarmCmd->>Manifest: register worktrees & agents
    Manifest-->>SwarmCmd: updated
    SwarmCmd-->>User: output JSON or human-readable result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hopping through templates, prompts in tow,

Agents gather where the code reviews grow.
TMUX lights flicker, worktrees take flight,
Bundled swarms hum softly into the night. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: addition of swarm templates for multi-agent workflows, which is the core feature introduced across multiple files.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering what was added, why, how it was implemented, and providing a detailed test plan with specific verification steps.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ppg/templates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Validate template name argument and YAML swarm name against SAFE_NAME
regex before using as filesystem path or branch name components. Return
new object from loadSwarm instead of mutating parsed YAML in place.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f390ebffed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/commands/swarm.ts Outdated
Comment on lines +194 to +195
const wtName = `${baseName}-${swarmAgent.prompt}`;
const branchName = `ppg/${wtName}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make isolated swarm worktree names collision-safe

In isolated mode, each branch/worktree name is derived only from swarmAgent.prompt, so a template with two agents sharing the same prompt (for example, same prompt text with different agent backends) produces duplicate ppg/<base>-<prompt> branch names. The second createWorktree call then fails because the branch already exists, aborting the swarm before all agents are launched.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in commit cf7e984. swarmIsolated() now tracks used names in a Set and appends an incrementing suffix (e.g., -2, -3) when a duplicate baseName-prompt combination is detected. This ensures unique branch names even when multiple agents share the same prompt name.

See src/commands/swarm.ts lines 192-199.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in cf7e984. Isolated mode now tracks used names with a Set<string> and appends a numeric suffix (-2, -3, etc.) when a collision is detected. This handles templates with duplicate prompt references without aborting the swarm.

The fix is in src/commands/swarm.ts:192-201. Tests added in src/core/swarm.test.ts to cover the dedup behavior.

Comment thread src/commands/list.ts Outdated
swarmNames.map(async (name) => {
const swarm = await loadSwarm(projectRoot, name);
return {
name: swarm.name,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge List swarms using the invokable template key

This command loads swarm files by filename (name from listSwarms) but reports name: swarm.name, which can differ from the filename; in that case ppg list swarms advertises a name users cannot actually pass to ppg swarm <template>. For example, legacy.yml with name: code-review will be listed as code-review, but only ppg swarm legacy resolves.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in commit cf7e984. listSwarmsCommand() now displays the filename-based key (from listSwarms()) instead of swarm.name from the YAML content, so the listed name matches what users pass to ppg swarm <template>.

See src/commands/list.ts line 82: changed name: swarm.name to name (the filename key).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in cf7e984. ppg list swarms now displays the filename key (the value returned by listSwarms()) instead of swarm.name. This ensures the listed name matches what ppg swarm <template> actually resolves.

The change is a one-liner in src/commands/list.ts:82: name: swarm.namename (uses the filename key from the map callback).

Comment thread src/core/swarm.ts
Comment on lines +38 to +42
let filePath = path.join(dir, `${name}.yaml`);
try {
await fs.access(filePath);
} catch {
filePath = path.join(dir, `${name}.yml`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject traversal in swarm template names

The template argument is concatenated into path.join(dir, ${name}.yaml)/.yml without validating name, so inputs like ../../tmp/custom resolve outside .pg/swarms and allow loading arbitrary YAML files from elsewhere on disk. This breaks the documented scope of swarm templates and should be blocked with a safe-name check before path construction.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in commit b7e8930. loadSwarm() now validates the template name argument against SAFE_NAME (/^[\w-]+$/) before constructing any path, rejecting traversal patterns like ../../tmp/custom. The YAML name field is also validated the same way. Tests added in swarm.test.ts covering path traversal, slashes, and spaces for both inputs.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already addressed in b7e8930. The SAFE_NAME regex (/^[\w-]+$/) validation was present in the original commit at src/core/swarm.ts:35-40 — it rejects any name containing path traversal characters (., /, \) before the path is constructed.

Additionally, parsed.name and each agent.prompt are also validated against SAFE_NAME during template loading (lines 64 and 87).

Add index suffix to deduplicate worktree names when isolated strategy
has agents with the same prompt. Show filename-based key in list swarms
output so users can invoke the correct template name.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/init.ts (1)

209-215: ⚠️ Potential issue | 🟡 Minor

Inconsistency: .pg/swarms/ is not gitignored while .pg/prompts/ is.

After ppg init, .pg/swarms/code-review.yaml will appear as an untracked file in git, while .pg/prompts/review-*.md files are invisible (gitignored). Since both are written from the same bundled-content workflow and regenerated on each ppg init, they should be treated consistently — either both gitignored (generated artifacts) or both tracked (shareable config).

🔍 Proposed fix (treating swarms as tracked config)

If swarm templates should be team-shareable (committed), remove .pg/prompts/ from the gitignore list and let users decide for both. If both are generated artifacts, add .pg/swarms/ to the list:

   const entriesToAdd = [
     '.pg/results/',
     '.pg/logs/',
     '.pg/manifest.json',
     '.pg/prompts/',
     '.pg/conductor-context.md',
+    '.pg/swarms/',
   ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/init.ts` around lines 209 - 215, The entriesToAdd array
currently gitignores '.pg/prompts/' but not '.pg/swarms/', causing
'.pg/swarms/code-review.yaml' to be left untracked; update the entriesToAdd
constant to include '.pg/swarms/' (or, if you prefer to keep swarms tracked,
remove '.pg/prompts/' instead) so both '.pg/prompts/' and '.pg/swarms/' are
treated consistently; modify the entriesToAdd array in init.ts to reflect the
chosen option.
🧹 Nitpick comments (2)
src/commands/swarm.ts (2)

92-101: Template-level agent vars silently override user --var values.

The spread order on lines 99-100 means swarmAgent.vars wins over userVars when keys collide. If this is intentional (template author locks values), consider documenting it. If user vars should take precedence, swap the spread order.

    ...userVars,
    ...(swarmAgent.vars ?? {}),   // ← overwrites matching userVars keys
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/swarm.ts` around lines 92 - 101, The TemplateContext
construction currently spreads userVars then swarmAgent.vars so template-level
vars in swarmAgent.vars override user-specified --var values; update the spread
order in the ctx object so that swarmAgent.vars is spread first and userVars
spread after (i.e., ensure userVars wins on key collisions), or if the opposite
behavior is intentional, add clear documentation/comments near
TemplateContext/ctx explaining that swarmAgent.vars intentionally lock/override
user vars.

140-168: Partial failure leaves spawned agents untracked in the manifest.

If spawnSwarmAgent throws for the N-th agent, agents 0…N-1 are already running in tmux but the manifest is never updated (line 165 is only reached after the entire loop completes). Those orphaned agents won't appear in ppg status or ppg kill --all.

Consider writing agents into the manifest incrementally, or wrapping the loop in a try/catch that persists whatever was successfully spawned before re-throwing.

The same pattern exists in swarmIntoExistingWorktree (lines 275-297). swarmIsolated already handles this correctly by updating the manifest per iteration.

♻️ Sketch: incremental manifest update for swarmShared
+ // Create the worktree entry early so partial spawns are tracked
+ const worktreeEntry: WorktreeEntry = {
+   id: wtId,
+   name,
+   path: wtPath,
+   branch: branchName,
+   baseBranch,
+   status: 'active',
+   tmuxWindow: windowTarget,
+   agents: {},
+   createdAt: new Date().toISOString(),
+ };
+
+ await updateManifest(projectRoot, (m) => {
+   m.worktrees[wtId] = worktreeEntry;
+   return m;
+ });
+
  const agents: AgentEntry[] = [];
  for (let i = 0; i < swarm.agents.length; i++) {
    const target = i === 0
      ? windowTarget
      : await tmux.createWindow(sessionName, `${name}-${i}`, wtPath);

    const agentEntry = await spawnSwarmAgent({
      projectRoot, config, swarmAgent: swarm.agents[i],
      wtPath, branchName, tmuxTarget: target, taskName: name, userVars,
    });
    agents.push(agentEntry);

+   await updateManifest(projectRoot, (m) => {
+     m.worktrees[wtId].agents[agentEntry.id] = agentEntry;
+     return m;
+   });
  }

- const worktreeEntry: WorktreeEntry = { ... };
- await updateManifest(projectRoot, (m) => {
-   m.worktrees[wtId] = worktreeEntry;
-   return m;
- });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/swarm.ts` around lines 140 - 168, The loop that calls
spawnSwarmAgent inside the swarm command can leave already-started agents
unrecorded if a later spawn throws; persist each successful agent to the
manifest as you go (or catch errors, write the partial state, then re-throw) so
orphaned tmux agents are tracked. Specifically, after each successful
spawnSwarmAgent return (inside the for loop that builds agents[] and before
continuing to the next iteration), call updateManifest to add that agent into
m.worktrees[wtId].agents (or update the existing worktree entry) so partial
successes are durable; apply the same defensive change to
swarmIntoExistingWorktree where the same pattern appears and ensure
updateManifest usage references the same worktree id and agent.id keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@skills/ppg-conductor/references/commands.md`:
- Around line 216-217: Add a blank line between the "**Options:**" heading and
the table header to satisfy markdownlint rule MD058; locate the "**Options:**"
text in references/commands.md and insert one empty line immediately after it so
the table starts on the next line.

In `@src/commands/init.ts`:
- Around line 113-121: Replace the inline path construction with the existing
helper: import promptFile (add it to the imports on line 5) and use
promptFile(projectRoot, name) instead of path.join(promptsDir(projectRoot),
`${name}.md`) inside the loop where bundledPrompts is iterated (the block that
currently assigns pPath and calls fs.access/fs.writeFile and info). Keep the
rest of the logic (try/catch, fs.access, fs.writeFile, info) unchanged.

In `@src/commands/list.ts`:
- Around line 73-76: The early-return for empty swarms (the if
(swarmNames.length === 0) block) currently prints plain text and returns; update
it to honor the command's JSON flag by checking the flag (the same --json/flags
variable used elsewhere in this file) and when JSON is requested print a
machine-readable empty result like JSON.stringify({swarms: []}) to stdout,
otherwise keep the existing console.log('No swarm templates found in
.pg/swarms/'); then return as before. Ensure you reference the existing json
flag variable used by this command so the behavior matches other code paths.

In `@src/core/swarm.test.ts`:
- Around line 27-34: The test assumes stable ordering from fs.readdir; update
listSwarms to return a deterministically ordered list (e.g., sort the filenames
after stripping the .yaml extension) so consumers and tests get a stable,
alphabetical array, and then update/keep the test ('given a directory with .yaml
files, should return names without extension') to assert the sorted result;
locate and change the listSwarms function to perform the sort before returning
the names.
- Around line 151-154: The test reveals that loadSwarm's parsing step can return
null (yaml.parse('') === null) and the subsequent guard in loadSwarm that checks
parsed.name/agents causes a TypeError instead of the intended PgError
INVALID_ARGS; update loadSwarm to first check for a null/undefined parsed result
(e.g., if (!parsed) throw new PgError('missing name or agents', 'INVALID_ARGS'))
before accessing parsed.name or parsed.agents so empty YAML files produce the
proper INVALID_ARGS PgError (add the corresponding test that writes an
empty.yaml and expects INVALID_ARGS to confirm the behavior).

In `@src/core/swarm.ts`:
- Around line 50-55: YAML.parse can return null for empty files, so add a
null-safe check immediately after calling YAML.parse in the function where
`parsed` is created (the `parsed` variable from `YAML.parse` cast to
`SwarmTemplate`) and before accessing `parsed.name` or `parsed.agents`; replace
the current validation with a guard like "if (!parsed || typeof parsed !==
'object' || !parsed.name || !parsed.agents || !Array.isArray(parsed.agents))
throw new PgError(...)" so the code throws the intended PgError (same message
and code) instead of allowing a TypeError.

---

Outside diff comments:
In `@src/commands/init.ts`:
- Around line 209-215: The entriesToAdd array currently gitignores
'.pg/prompts/' but not '.pg/swarms/', causing '.pg/swarms/code-review.yaml' to
be left untracked; update the entriesToAdd constant to include '.pg/swarms/'
(or, if you prefer to keep swarms tracked, remove '.pg/prompts/' instead) so
both '.pg/prompts/' and '.pg/swarms/' are treated consistently; modify the
entriesToAdd array in init.ts to reflect the chosen option.

---

Nitpick comments:
In `@src/commands/swarm.ts`:
- Around line 92-101: The TemplateContext construction currently spreads
userVars then swarmAgent.vars so template-level vars in swarmAgent.vars override
user-specified --var values; update the spread order in the ctx object so that
swarmAgent.vars is spread first and userVars spread after (i.e., ensure userVars
wins on key collisions), or if the opposite behavior is intentional, add clear
documentation/comments near TemplateContext/ctx explaining that swarmAgent.vars
intentionally lock/override user vars.
- Around line 140-168: The loop that calls spawnSwarmAgent inside the swarm
command can leave already-started agents unrecorded if a later spawn throws;
persist each successful agent to the manifest as you go (or catch errors, write
the partial state, then re-throw) so orphaned tmux agents are tracked.
Specifically, after each successful spawnSwarmAgent return (inside the for loop
that builds agents[] and before continuing to the next iteration), call
updateManifest to add that agent into m.worktrees[wtId].agents (or update the
existing worktree entry) so partial successes are durable; apply the same
defensive change to swarmIntoExistingWorktree where the same pattern appears and
ensure updateManifest usage references the same worktree id and agent.id keys.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a17cbc6 and f390ebf.

📒 Files selected for processing (12)
  • skills/ppg-conductor/references/commands.md
  • skills/ppg-conductor/references/conductor.md
  • skills/ppg-conductor/references/modes.md
  • src/bundled/prompts.ts
  • src/bundled/swarms.ts
  • src/cli.ts
  • src/commands/init.ts
  • src/commands/list.ts
  • src/commands/swarm.ts
  • src/core/swarm.test.ts
  • src/core/swarm.ts
  • src/lib/paths.ts

Comment thread skills/ppg-conductor/references/commands.md
Comment thread src/commands/init.ts
Comment thread src/commands/list.ts
Comment thread src/core/swarm.test.ts
Comment thread src/core/swarm.test.ts
Comment thread src/core/swarm.ts
…leanup

- Add null guard after YAML.parse to handle empty/malformed YAML files
- Honor --json flag in empty swarms early exit for machine-readable output
- Use promptFile() path helper in init.ts instead of inline construction
- Add blank lines before tables in commands.md (MD058)
- Use order-independent assertion for listSwarms test
- Add test case for empty YAML file edge case
…ence

- Write worktree to manifest before spawning agents and add each agent
  incrementally so partial failures leave tracked state
- User --var flags now take precedence over template-level agent vars
- Apply same incremental pattern to swarmIntoExistingWorktree

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/list.ts (1)

24-32: ⚠️ Potential issue | 🟡 Minor

Honor --json when no templates exist.
The empty-templates early return prints plain text even when JSON is requested, which breaks machine-readable output.

🔧 Proposed fix
   if (templateNames.length === 0) {
-    console.log('No templates found in .pg/templates/');
+    if (options.json) {
+      output({ templates: [] }, true);
+    } else {
+      console.log('No templates found in .pg/templates/');
+    }
     return;
   }

As per coding guidelines: "Every command must support --json flag for machine-readable output. Use output(data, json) and outputError(error, json) from lib/output.ts."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/list.ts` around lines 24 - 32, The early-return in
listTemplatesCommand currently prints plain text when templateNames is empty;
change it to honor options.json by calling output({ templates: [] },
options.json) (or outputError if you want an error payload) instead of
console.log, i.e. import and use output/exported helpers from lib/output.ts and
replace the console.log('No templates...') branch so it returns a
machine-readable JSON payload when options.json is true; reference
listTemplatesCommand, ListOptions, templateNames, output and outputError to
locate and update the logic accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/swarm.ts`:
- Around line 193-237: The worktree is created (createWorktree,
tmux.createWindow) but only written to the manifest after spawnSwarmAgent
succeeds, so if spawnSwarmAgent throws the worktree and tmux window are
orphaned; fix by registering the worktreeEntry in updateManifest immediately
after creating the worktree/tmux (set agents: {} initially and status 'active'),
then call spawnSwarmAgent and, on success, update the manifest entry to add the
returned agentEntry; also wrap spawnSwarmAgent in try/catch to rollback (remove
manifest entry and close tmux window / delete worktree) if it throws to avoid
orphaned resources (refer to functions/genWorktreeId, worktreeEntry,
spawnSwarmAgent, updateManifest, tmux.createWindow).

---

Outside diff comments:
In `@src/commands/list.ts`:
- Around line 24-32: The early-return in listTemplatesCommand currently prints
plain text when templateNames is empty; change it to honor options.json by
calling output({ templates: [] }, options.json) (or outputError if you want an
error payload) instead of console.log, i.e. import and use output/exported
helpers from lib/output.ts and replace the console.log('No templates...') branch
so it returns a machine-readable JSON payload when options.json is true;
reference listTemplatesCommand, ListOptions, templateNames, output and
outputError to locate and update the logic accordingly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f390ebf and 09537b4.

📒 Files selected for processing (6)
  • skills/ppg-conductor/references/commands.md
  • src/commands/init.ts
  • src/commands/list.ts
  • src/commands/swarm.ts
  • src/core/swarm.test.ts
  • src/core/swarm.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/core/swarm.test.ts
  • src/commands/init.ts
  • src/core/swarm.ts

Comment thread src/commands/swarm.ts
Comment on lines +193 to +237
const worktrees: Array<{ id: string; name: string; branch: string; path: string; tmuxWindow: string }> = [];
const allAgents: AgentEntry[] = [];

const usedNames = new Set<string>();
for (const swarmAgent of swarm.agents) {
const wtId = genWorktreeId();
let wtName = `${baseName}-${swarmAgent.prompt}`;
if (usedNames.has(wtName)) {
let suffix = 2;
while (usedNames.has(`${wtName}-${suffix}`)) suffix++;
wtName = `${wtName}-${suffix}`;
}
usedNames.add(wtName);
const branchName = `ppg/${wtName}`;

info(`Creating worktree ${wtId} on branch ${branchName}`);
const wtPath = await createWorktree(projectRoot, wtId, {
branch: branchName,
base: baseBranch,
});

await setupWorktreeEnv(projectRoot, wtPath, config);
const windowTarget = await tmux.createWindow(sessionName, wtName, wtPath);

const agentEntry = await spawnSwarmAgent({
projectRoot, config, swarmAgent,
wtPath, branchName, tmuxTarget: windowTarget, taskName: wtName, userVars,
});

const worktreeEntry: WorktreeEntry = {
id: wtId,
name: wtName,
path: wtPath,
branch: branchName,
baseBranch,
status: 'active',
tmuxWindow: windowTarget,
agents: { [agentEntry.id]: agentEntry },
createdAt: new Date().toISOString(),
};

await updateManifest(projectRoot, (m) => {
m.worktrees[wtId] = worktreeEntry;
return m;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Register isolated worktrees before spawning agents.
If spawnSwarmAgent throws, the worktree and tmux window are created but never recorded in the manifest, leaving orphaned resources and making cleanup harder.

🛠️ Proposed fix
-    const agentEntry = await spawnSwarmAgent({
-      projectRoot, config, swarmAgent,
-      wtPath, branchName, tmuxTarget: windowTarget, taskName: wtName, userVars,
-    });
-
-    const worktreeEntry: WorktreeEntry = {
+    const worktreeEntry: WorktreeEntry = {
       id: wtId,
       name: wtName,
       path: wtPath,
       branch: branchName,
       baseBranch,
       status: 'active',
       tmuxWindow: windowTarget,
-      agents: { [agentEntry.id]: agentEntry },
+      agents: {},
       createdAt: new Date().toISOString(),
     };
 
     await updateManifest(projectRoot, (m) => {
       m.worktrees[wtId] = worktreeEntry;
       return m;
     });
+
+    const agentEntry = await spawnSwarmAgent({
+      projectRoot, config, swarmAgent,
+      wtPath, branchName, tmuxTarget: windowTarget, taskName: wtName, userVars,
+    });
+
+    await updateManifest(projectRoot, (m) => {
+      m.worktrees[wtId].agents[agentEntry.id] = agentEntry;
+      return m;
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/swarm.ts` around lines 193 - 237, The worktree is created
(createWorktree, tmux.createWindow) but only written to the manifest after
spawnSwarmAgent succeeds, so if spawnSwarmAgent throws the worktree and tmux
window are orphaned; fix by registering the worktreeEntry in updateManifest
immediately after creating the worktree/tmux (set agents: {} initially and
status 'active'), then call spawnSwarmAgent and, on success, update the manifest
entry to add the returned agentEntry; also wrap spawnSwarmAgent in try/catch to
rollback (remove manifest entry and close tmux window / delete worktree) if it
throws to avoid orphaned resources (refer to functions/genWorktreeId,
worktreeEntry, spawnSwarmAgent, updateManifest, tmux.createWindow).

@2witstudios

Copy link
Copy Markdown
Owner Author

Addressing CodeRabbit review body items

Responding to the 3 items from the CodeRabbit review body that were outside the inline comment scope:

1. Outside diff: .pg/swarms/ not gitignored (init.ts:209-215)

Already fixed.pg/swarms/ was added to the entriesToAdd array in src/commands/init.ts:214 in a prior commit. Both .pg/prompts/ and .pg/swarms/ are now consistently gitignored as generated artifacts.

2. Nitpick: Template vars override user vars (swarm.ts:92-101)

Already fixed — The spread order was corrected so swarmAgent.vars is spread first, then userVars, ensuring user --var values take precedence over template-level defaults (src/commands/swarm.ts:99-100).

3. Nitpick: Partial failure leaves agents untracked (swarm.ts:140-168)

Already fixed — Both swarmShared and swarmIntoExistingWorktree now register the worktree entry in the manifest before the spawn loop and add each agent incrementally via updateManifest after each successful spawn (src/commands/swarm.ts:140-172 and src/commands/swarm.ts:286-310). This ensures partial failures don't leave orphaned agents.

@2witstudios
2witstudios merged commit 1f7f154 into main Feb 24, 2026
3 checks passed
@2witstudios
2witstudios deleted the ppg/templates branch February 26, 2026 03:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant