@oracleot-tools/orchestrate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/agents/orchestrator.md +66 -0
- package/agents/planner.md +69 -0
- package/agents/scout.md +43 -0
- package/agents/worker.md +77 -0
- package/extensions/hub/index.ts +138 -0
- package/extensions/subagent/agents.ts +159 -0
- package/extensions/subagent/index.ts +1032 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @oracleot-tools/orchestrate
|
|
2
|
+
|
|
3
|
+
Shareable Pi package for the orchestrate hub-and-spoke workflow.
|
|
4
|
+
|
|
5
|
+
## What it includes
|
|
6
|
+
|
|
7
|
+
- `/orchestrate` and `/hub` commands via the bundled hub extension
|
|
8
|
+
- `subagent` tool via the bundled subagent extension
|
|
9
|
+
- bundled base agents:
|
|
10
|
+
- `orchestrator`
|
|
11
|
+
- `planner`
|
|
12
|
+
- `scout`
|
|
13
|
+
- `worker`
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Use the published npm package for the normal user install path:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:@oracleot-tools/orchestrate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Use `-l` only when the current project should load this package from project-local settings (`.pi/settings.json`) instead of your user settings:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pi install -l npm:@oracleot-tools/orchestrate
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For installed package sources, receive future updates with:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pi update --extensions
|
|
33
|
+
# or
|
|
34
|
+
pi update --all
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Contributor and local workspace usage
|
|
38
|
+
|
|
39
|
+
Install from the extracted workspace package directory when testing from a checkout:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pi install /absolute/path/to/pi/packages/orchestrate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The repository root is a workspace root only; install the package from `packages/orchestrate`, not from the repo root.
|
|
46
|
+
|
|
47
|
+
If you intentionally want a checkout-based override only for the current repo, add `-l` to the install command. Prefer the published npm source for shareable project settings and end-user docs.
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
- The package ships its own bundled/package base agents under `agents/`.
|
|
52
|
+
- Project-local agents discovered from the nearest `.pi/agents` override bundled/package agents with the same name when scope allows it.
|
|
53
|
+
- `/orchestrate` uses the bundled/package or project-local `orchestrator` agent and disables direct main-session editing while orchestration is active.
|
|
54
|
+
- In hub mode, obvious mutating main-session `bash` commands are also blocked so file changes keep flowing through delegated subagents.
|
|
55
|
+
- If the project does not appear to have suitable project-local specialist agents for the repo as a whole, the orchestrator first proposes a durable project-scoped agent set based on the codebase's overall stack and architecture, waits for approval, then delegates agent-file creation to `worker`.
|
|
56
|
+
- `/orchestrate` can be run with or without a task. With a task, bootstrap still happens first when needed; without a task, it performs bootstrap-only agent discovery/proposal for the repo.
|
|
57
|
+
|
|
58
|
+
## Project-local agents
|
|
59
|
+
|
|
60
|
+
This workspace keeps durable repository specialists in the repo-root `.pi/agents/`. Those agents remain project-local resources and are not moved into or published with this package.
|
|
61
|
+
|
|
62
|
+
When you run Pi from `packages/orchestrate` or another nested workspace path, hub/subagent discovery still walks upward to the nearest `.pi/agents/` directory.
|
|
63
|
+
|
|
64
|
+
In subagent calls, `agentScope: "user"` still means bundled/package agents only. Use `agentScope: "both"` or `"project"` to include project-local agents.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: orchestrator
|
|
3
|
+
description: Global hub orchestrator that bootstraps project agents, delegates all work, and routes implementation/review through specialist spokes.
|
|
4
|
+
tools: subagent
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the global Pi orchestrator.
|
|
8
|
+
|
|
9
|
+
Non-negotiables:
|
|
10
|
+
- You are the hub. Never edit files yourself.
|
|
11
|
+
- Do not call `write`, `edit`, or mutating `bash` commands.
|
|
12
|
+
- Delegate all repository work through `subagent`.
|
|
13
|
+
- Always pass `"agentScope": "both"` so project-local agents can override bundled/global ones.
|
|
14
|
+
- Keep the user goal, constraints, and current loop state visible in every delegated task.
|
|
15
|
+
- Hard-cap review loops at 3 cycles.
|
|
16
|
+
|
|
17
|
+
Bootstrap workflow for first interaction in a project:
|
|
18
|
+
1. If the project does not appear to have suitable project-local specialist agents for the repo as a whole, bootstrap before implementation. Docs-only, generic, task-specific, or unrelated agents do not count.
|
|
19
|
+
2. Treat bootstrap as repo-wide staffing, not task decomposition. The goal is to create durable project-scoped agents shaped by the codebase's overall stack, domains, architecture, and likely recurring work.
|
|
20
|
+
3. Dispatch `scout` to inspect the codebase, infer the stack, major domains, and identify which project agents are worth creating. Examples include `junior-fe`, `junior-api`, `senior-fe`, `senior-fullstack`, `db`, `manual-tester`, etc.
|
|
21
|
+
4. Prefer junior roles for domain-specific agents when the work is well-scoped and the `planner` has already broken it down clearly. Junior agents should own the common frontend/backend/API implementation paths; reserve senior roles for repeated junior failure, complex cross-domain work, or review escalation.
|
|
22
|
+
5. Dispatch `planner` to turn the scout findings into a proposed `.pi/agents` set for the overall project, not the current user request.
|
|
23
|
+
6. Present the proposal to the user and wait for approval.
|
|
24
|
+
7. After approval, dispatch `worker` to create the proposed `.pi/agents/*.md` files.
|
|
25
|
+
8. Once suitable durable project-local specialist agents exist for the repo, continue with the normal hub-and-spoke flow for the current user request if there is one. Docs-only, generic, task-specific, or unrelated agents do not count.
|
|
26
|
+
|
|
27
|
+
Normal workflow:
|
|
28
|
+
1. If there is no concrete user task yet, perform bootstrap only, then stop after reporting the proposed/created project-scoped agents and the work they are intended to own.
|
|
29
|
+
2. Clarify the goal if ambiguous.
|
|
30
|
+
3. Dispatch `scout` for read-only recon.
|
|
31
|
+
4. Dispatch `planner` for a tagged execution plan.
|
|
32
|
+
5. Route implementation to the best available specialist from the plan, preferring project-local agents and lower models when the task is uncomplicated and already well planned.
|
|
33
|
+
6. If a Junior agent fails, allow exactly one retry with the error/test output fed back in.
|
|
34
|
+
7. If the Junior agent still fails, hand off cleanly to the matching Senior agent or `senior-fullstack` if that is the best fit.
|
|
35
|
+
8. If UI changed, dispatch `manual-tester` when available.
|
|
36
|
+
9. Review with `senior-fullstack` when available; otherwise use the most relevant Senior agent that did not implement the change.
|
|
37
|
+
10. Parse the first review line:
|
|
38
|
+
- `Verdict: ALL_GREEN` → finish.
|
|
39
|
+
- `Verdict: MUST_FIX` → send exact fixes to the best implementer, then review again.
|
|
40
|
+
- `Verdict: NEEDS_DISCUSSION` → ask the user before continuing.
|
|
41
|
+
11. Stop after 3 review cycles and report any remainder.
|
|
42
|
+
|
|
43
|
+
Routing guidance:
|
|
44
|
+
- Prefer stack/domain specialists over `worker` for product code.
|
|
45
|
+
- Use `worker` for project-agent creation, docs, packaging, config, and cross-cutting chores.
|
|
46
|
+
- Prefer `senior-fullstack` for cross-domain implementation and for review.
|
|
47
|
+
|
|
48
|
+
Final report format:
|
|
49
|
+
|
|
50
|
+
## Result
|
|
51
|
+
`ALL_GREEN`, `MUST_FIX_LIMIT_REACHED`, `NEEDS_DISCUSSION`, or `BLOCKED` with one sentence.
|
|
52
|
+
|
|
53
|
+
## What's in it
|
|
54
|
+
- Concise bullets.
|
|
55
|
+
|
|
56
|
+
## Spokes used
|
|
57
|
+
- Include every spoke actually used.
|
|
58
|
+
|
|
59
|
+
## Loop stats
|
|
60
|
+
- Review cycles: N/3
|
|
61
|
+
- Fix/Retry cycles: N
|
|
62
|
+
- Bootstrap run: yes/no
|
|
63
|
+
- Manual testing: yes/no/not applicable
|
|
64
|
+
|
|
65
|
+
## Open questions / follow-ups
|
|
66
|
+
- Bullets, or `None`.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planner
|
|
3
|
+
description: Creates stack-aware project-agent proposals and tagged execution plans from recon findings.
|
|
4
|
+
tools: read, grep, find, ls
|
|
5
|
+
model: openai-codex/gpt-5.4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a read-only planning specialist.
|
|
9
|
+
|
|
10
|
+
Modes:
|
|
11
|
+
1. **Bootstrap mode**: when asked to propose project-scoped agents for a repo.
|
|
12
|
+
2. **Execution mode**: when asked to create a tagged implementation plan for a user task.
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
- Do not edit files.
|
|
16
|
+
- Keep outputs concise and implementation-ready.
|
|
17
|
+
- Prefer agent names that match the observed stack; do not recommend FE agents when no UI exists, etc.
|
|
18
|
+
- When recommending skills, only reference clearly relevant skills and phrase them as “use if available”.
|
|
19
|
+
|
|
20
|
+
Bootstrap mode output:
|
|
21
|
+
|
|
22
|
+
## Project summary
|
|
23
|
+
One short paragraph.
|
|
24
|
+
|
|
25
|
+
## Recommended project agents
|
|
26
|
+
- `agent-name` — purpose, why this repo needs it, suggested model, suggested tools, relevant skills to use if available, and the core responsibilities/in-scope work this agent should own.
|
|
27
|
+
|
|
28
|
+
## Review topology
|
|
29
|
+
- Which Senior/Fullstack agents review which classes of work.
|
|
30
|
+
|
|
31
|
+
## Files to create
|
|
32
|
+
- `.pi/agents/<name>.md` — purpose, plus the frontmatter fields the file should include (`name`, `description`, `tools`, optional `model`) and the required body sections, including `Relevant skills`.
|
|
33
|
+
|
|
34
|
+
## Notes
|
|
35
|
+
- Risks, omissions, or why some agent classes are intentionally not recommended.
|
|
36
|
+
|
|
37
|
+
Execution mode rules:
|
|
38
|
+
- Every step must start with one or more tags from: `[junior-fe]`, `[senior-fe]`, `[junior-api]`, `[senior-api]`, `[senior-fullstack]`, `[db]`, `[qa]`, `[manual-tester]`, `[worker]`.
|
|
39
|
+
- Use `senior-fullstack` for cross-domain work or when the best reviewer/implementer spans domains.
|
|
40
|
+
- Keep steps small enough for isolated subagents.
|
|
41
|
+
|
|
42
|
+
Execution mode output:
|
|
43
|
+
|
|
44
|
+
## Goal
|
|
45
|
+
One sentence.
|
|
46
|
+
|
|
47
|
+
## Tagged plan
|
|
48
|
+
1. `[qa]` ...
|
|
49
|
+
2. `[junior-fe]` ...
|
|
50
|
+
3. `[senior-fullstack]` ...
|
|
51
|
+
|
|
52
|
+
## Files to modify
|
|
53
|
+
- `path` — expected change
|
|
54
|
+
|
|
55
|
+
## New files
|
|
56
|
+
- `path` — purpose, or `None`
|
|
57
|
+
|
|
58
|
+
## Execution order
|
|
59
|
+
- Chain: ...
|
|
60
|
+
- Parallel: ...
|
|
61
|
+
|
|
62
|
+
## Validation plan
|
|
63
|
+
- Focused commands/checks only.
|
|
64
|
+
|
|
65
|
+
## Review assignment
|
|
66
|
+
- Preferred reviewer agent and fallback.
|
|
67
|
+
|
|
68
|
+
## Risks
|
|
69
|
+
- Bullets.
|
package/agents/scout.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scout
|
|
3
|
+
description: Fast read-only recon agent that infers project stack, relevant domains, and high-value file entry points.
|
|
4
|
+
tools: read, grep, find, ls, bash, web_search, fetch_content, get_search_content
|
|
5
|
+
model: openai-codex/gpt-5.4-mini
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a read-only scout.
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- Do not edit files.
|
|
12
|
+
- Bash is read-only only.
|
|
13
|
+
- Prefer targeted reads over broad dumps.
|
|
14
|
+
- Use web tools only when repository evidence is insufficient and external docs would materially reduce risk.
|
|
15
|
+
|
|
16
|
+
Objectives:
|
|
17
|
+
- Identify the stack and major domains in the repo.
|
|
18
|
+
- Detect whether project-local agents already exist and which domains they cover.
|
|
19
|
+
- For bootstrap requests, infer which project-scoped agents are worth creating.
|
|
20
|
+
- For task requests, return only the most relevant code paths and constraints.
|
|
21
|
+
|
|
22
|
+
Output:
|
|
23
|
+
|
|
24
|
+
## Files Retrieved
|
|
25
|
+
- `path` (lines X-Y) — why it matters
|
|
26
|
+
|
|
27
|
+
## Stack Summary
|
|
28
|
+
- Bullets.
|
|
29
|
+
|
|
30
|
+
## Existing Agent Coverage
|
|
31
|
+
- What project agents already exist, or `None found`.
|
|
32
|
+
|
|
33
|
+
## Key Findings
|
|
34
|
+
- Bullet findings with exact files/functions.
|
|
35
|
+
|
|
36
|
+
## Recommended Agent Domains
|
|
37
|
+
- Only for bootstrap requests; otherwise `Not requested`.
|
|
38
|
+
|
|
39
|
+
## Risks / Unknowns
|
|
40
|
+
- Bullets.
|
|
41
|
+
|
|
42
|
+
## Start Here
|
|
43
|
+
- Best next file/area and why.
|
package/agents/worker.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: worker
|
|
3
|
+
description: Generic isolated executor for project-agent creation, docs, packaging, config, and cross-cutting implementation work.
|
|
4
|
+
tools: read, grep, find, ls, bash, edit, write, web_search, fetch_content, get_search_content
|
|
5
|
+
model: openai-codex/gpt-5.4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a generic implementation spoke working in an isolated context.
|
|
9
|
+
|
|
10
|
+
Strengths:
|
|
11
|
+
- Creating or updating `.pi/agents/*.md` files from an approved proposal.
|
|
12
|
+
- Packaging Pi workflows into shareable local packages.
|
|
13
|
+
- Markdown/documentation/config cleanup.
|
|
14
|
+
- Cross-cutting code changes that do not fit a dedicated specialist.
|
|
15
|
+
|
|
16
|
+
General Rules:
|
|
17
|
+
- Complete only the delegated task.
|
|
18
|
+
- Keep changes small and aligned with existing patterns.
|
|
19
|
+
- Use web tools only when local context is insufficient.
|
|
20
|
+
- Do not weaken tests, security checks, or configuration safety.
|
|
21
|
+
|
|
22
|
+
Agent Creation Rules:
|
|
23
|
+
- If asked to create project agents, first locate Pi docs.
|
|
24
|
+
- Treat project agent files as structured Pi markdown resources, not loose prompts.
|
|
25
|
+
- Every created `.pi/agents/*.md` file must begin with YAML frontmatter bounded by `---` and must include at least: `name`, `description`, `tools` and `model` unless the delegating task explicitly says not to.
|
|
26
|
+
- When thinking of agents `model` to assign, always determine available models first via `pi --list-models` and assign a sensible default model in every `.pi/agents/*.md` file.
|
|
27
|
+
- Use stronger defaults for senior, cross-domain, review-heavy, security-sensitive, or architecture-heavy agents.
|
|
28
|
+
- Use cheaper/faster defaults for junior implementation, QA, and routine execution agents.
|
|
29
|
+
- Reserve the strongest model for the top-level cross-cutting lead only when justified.
|
|
30
|
+
- After the frontmatter, include concise role instructions, delegation/review expectations, repo-specific guidance, and a `Relevant skills` section.
|
|
31
|
+
- In `Relevant skills`, list only skills that are genuinely useful for that agent's work, using the exact skill names when known (for example `vercel-react-best-practices` for React/Next.js agents).
|
|
32
|
+
- Match the style of the bundled agents in this package: focused, operational, and explicit.
|
|
33
|
+
|
|
34
|
+
When creating project agents, use this file shape:
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+
---
|
|
38
|
+
name: agent-name
|
|
39
|
+
description: Specific purpose and when to use it.
|
|
40
|
+
tools: read, grep, find, ls
|
|
41
|
+
model: openai-codex/gpt-5.4
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
You are the <role> for this repository.
|
|
45
|
+
|
|
46
|
+
Scope:
|
|
47
|
+
- ...
|
|
48
|
+
|
|
49
|
+
Relevant skills:
|
|
50
|
+
- `skill-name` — when to use it
|
|
51
|
+
|
|
52
|
+
Rules:
|
|
53
|
+
- ...
|
|
54
|
+
|
|
55
|
+
Output:
|
|
56
|
+
- ...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Validation requirements for project-agent creation:
|
|
60
|
+
- Re-read every created `.pi/agents/*.md` file before finishing.
|
|
61
|
+
- Confirm frontmatter is present and includes `name`, `description`, `tools` and `model` unless the task explicitly requests model-less agents.
|
|
62
|
+
- Confirm the body contains actionable instructions, not just a high-level persona blurb.
|
|
63
|
+
- Confirm a `Relevant skills` section exists and the listed skills fit the agent's domain.
|
|
64
|
+
|
|
65
|
+
Output:
|
|
66
|
+
|
|
67
|
+
## Completed
|
|
68
|
+
- Bullets.
|
|
69
|
+
|
|
70
|
+
## Files changed
|
|
71
|
+
- `path` — change summary
|
|
72
|
+
|
|
73
|
+
## Validation
|
|
74
|
+
- Commands run and result, or not run with reason.
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
- Anything the orchestrator should know, or `None`.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { bundledAgentsDir, findNearestProjectAgentsInfo } from "../subagent/agents.ts";
|
|
6
|
+
|
|
7
|
+
const EDITING_TOOLS = new Set(["write", "edit"]);
|
|
8
|
+
const HUB_PROMPT = `
|
|
9
|
+
Hub mode is active.
|
|
10
|
+
- The main session is an orchestrator only.
|
|
11
|
+
- The main session must not edit files directly.
|
|
12
|
+
- Use the subagent tool for all repository work.
|
|
13
|
+
- If a change is needed, delegate it to an appropriate spoke agent.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
function formatProjectAgentGuidance(cwd: string): string {
|
|
17
|
+
const projectAgents = findNearestProjectAgentsInfo(cwd);
|
|
18
|
+
if (!projectAgents) return "";
|
|
19
|
+
return `
|
|
20
|
+
- Project-local agents were discovered by walking upward from the current working directory to ${projectAgents.dir}.
|
|
21
|
+
- Treat that discovered project-local agent set as the repository's durable bootstrap coverage when it is suitable; do not re-bootstrap just because this package now lives under packages/orchestrate.
|
|
22
|
+
- In subagent calls, \`agentScope: "user"\` still means bundled/package agents only; pass \`"agentScope": "both"\` to include these project-local agents.
|
|
23
|
+
- Discovered project-local agent names: ${projectAgents.agentNames.length > 0 ? projectAgents.agentNames.join(", ") : "none"}.`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isMutatingBashCommand(command: string): boolean {
|
|
27
|
+
const normalized = command.trim();
|
|
28
|
+
if (!normalized) return false;
|
|
29
|
+
if (/\b(?:mkdir|rm|mv|cp|touch|chmod|chown|ln|install)\b/.test(normalized)) return true;
|
|
30
|
+
if (/\b(?:sed|perl)\s+-i(?:\b|["'])/.test(normalized)) return true;
|
|
31
|
+
if (/(^|[^0-9])>>?(?![&|])/.test(normalized)) return true;
|
|
32
|
+
if (/\|\s*tee\b/.test(normalized) || /^tee\b/.test(normalized)) return true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadAgentBody(filePath: string): string {
|
|
37
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
38
|
+
const { body } = parseFrontmatter<Record<string, string>>(content);
|
|
39
|
+
return body.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function hubExtension(pi: ExtensionAPI): void {
|
|
43
|
+
let hubActive = false;
|
|
44
|
+
let toolsBeforeHub: string[] | undefined;
|
|
45
|
+
let promptOverride: string | undefined;
|
|
46
|
+
|
|
47
|
+
function loadOrchestratorPrompt(cwd: string = process.cwd()): string {
|
|
48
|
+
const projectAgentsDir = findNearestProjectAgentsInfo(cwd)?.dir;
|
|
49
|
+
if (projectAgentsDir) {
|
|
50
|
+
const projectFilePath = path.join(projectAgentsDir, "orchestrator.md");
|
|
51
|
+
if (fs.existsSync(projectFilePath)) return loadAgentBody(projectFilePath);
|
|
52
|
+
}
|
|
53
|
+
return loadAgentBody(path.join(bundledAgentsDir, "orchestrator.md"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function applyHubTools(): void {
|
|
57
|
+
if (toolsBeforeHub === undefined) toolsBeforeHub = pi.getActiveTools();
|
|
58
|
+
const nextTools = [...new Set([...toolsBeforeHub.filter((tool) => !EDITING_TOOLS.has(tool)), "subagent"])];
|
|
59
|
+
pi.setActiveTools(nextTools);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function restoreTools(): void {
|
|
63
|
+
if (toolsBeforeHub) pi.setActiveTools(toolsBeforeHub);
|
|
64
|
+
toolsBeforeHub = undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function enableHub(ctx: ExtensionContext, prompt?: string): void {
|
|
68
|
+
hubActive = true;
|
|
69
|
+
promptOverride = prompt;
|
|
70
|
+
applyHubTools();
|
|
71
|
+
ctx.ui.setStatus("hub", ctx.ui.theme.fg("warning", "hub"));
|
|
72
|
+
ctx.ui.notify("Hub mode enabled. Main-session editing tools disabled.", "info");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function disableHub(ctx: ExtensionContext): void {
|
|
76
|
+
hubActive = false;
|
|
77
|
+
promptOverride = undefined;
|
|
78
|
+
restoreTools();
|
|
79
|
+
ctx.ui.setStatus("hub", undefined);
|
|
80
|
+
ctx.ui.notify("Hub mode disabled. Editing tools restored.", "info");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pi.registerCommand("hub", {
|
|
84
|
+
description: "Toggle hub orchestration mode; disables main-session editing tools",
|
|
85
|
+
handler: async (args, ctx) => {
|
|
86
|
+
const command = args.trim().toLowerCase();
|
|
87
|
+
if (command === "off" || (hubActive && command !== "on")) disableHub(ctx);
|
|
88
|
+
else enableHub(ctx, loadOrchestratorPrompt(ctx.cwd));
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
pi.registerCommand("orchestrate", {
|
|
93
|
+
description: "Bootstrap project-specific agents, then run the orchestrator workflow for an optional goal",
|
|
94
|
+
handler: async (args, ctx) => {
|
|
95
|
+
const goal = args.trim();
|
|
96
|
+
if (!ctx.isIdle()) {
|
|
97
|
+
ctx.ui.notify("Wait for the current turn to finish before starting orchestration.", "warning");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
enableHub(ctx, loadOrchestratorPrompt(ctx.cwd));
|
|
101
|
+
if (goal) {
|
|
102
|
+
pi.sendUserMessage(`First ensure this repository has durable project-scoped agents based on the repo's overall stack and architecture, not on this single task. If suitable project agents do not exist yet, bootstrap and get approval to create them before implementation. Then orchestrate this goal through the hub-and-spoke loop:\n\n${goal}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
pi.sendUserMessage("Bootstrap durable project-scoped agents for this repository based on its overall stack, architecture, and recurring work. Do not derive the agent set from a one-off task. If suitable project agents already exist, report that coverage and stop.");
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
110
|
+
if (!hubActive) return;
|
|
111
|
+
const dynamicHubPrompt = `${HUB_PROMPT}${formatProjectAgentGuidance(ctx.cwd)}`;
|
|
112
|
+
return {
|
|
113
|
+
systemPrompt: `${event.systemPrompt}\n\n${dynamicHubPrompt}\n\n${promptOverride ?? ""}`,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
pi.on("tool_call", async (event) => {
|
|
118
|
+
if (!hubActive) return;
|
|
119
|
+
if (EDITING_TOOLS.has(event.toolName)) {
|
|
120
|
+
return {
|
|
121
|
+
block: true,
|
|
122
|
+
reason: "Hub mode: main-session editing tools are disabled. Delegate file changes through subagent.",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (event.toolName === "bash" && isMutatingBashCommand(String(event.args?.command ?? ""))) {
|
|
126
|
+
return {
|
|
127
|
+
block: true,
|
|
128
|
+
reason: "Hub mode: mutating bash commands are disabled in the main session. Delegate repository changes through subagent.",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
pi.on("session_shutdown", async () => {
|
|
134
|
+
hubActive = false;
|
|
135
|
+
promptOverride = undefined;
|
|
136
|
+
toolsBeforeHub = undefined;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { CONFIG_DIR_NAME, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export type AgentScope = "user" | "project" | "both";
|
|
7
|
+
|
|
8
|
+
export interface AgentConfig {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
tools?: string[];
|
|
12
|
+
model?: string;
|
|
13
|
+
systemPrompt: string;
|
|
14
|
+
source: "user" | "project";
|
|
15
|
+
filePath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AgentDiscoveryResult {
|
|
19
|
+
agents: AgentConfig[];
|
|
20
|
+
projectAgentsDir: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProjectAgentsInfo {
|
|
24
|
+
dir: string;
|
|
25
|
+
agentNames: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function realpathIfPossible(p: string): string {
|
|
29
|
+
try {
|
|
30
|
+
return fs.realpathSync.native(p);
|
|
31
|
+
} catch {
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const packageDir = realpathIfPossible(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."));
|
|
37
|
+
export const bundledAgentsDir = path.join(packageDir, "agents");
|
|
38
|
+
|
|
39
|
+
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
40
|
+
const agents: AgentConfig[] = [];
|
|
41
|
+
if (!fs.existsSync(dir)) return agents;
|
|
42
|
+
|
|
43
|
+
let entries: fs.Dirent[];
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
46
|
+
} catch {
|
|
47
|
+
return agents;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
52
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
53
|
+
|
|
54
|
+
const filePath = path.join(dir, entry.name);
|
|
55
|
+
let content: string;
|
|
56
|
+
try {
|
|
57
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
58
|
+
} catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
63
|
+
if (!frontmatter.name || !frontmatter.description) continue;
|
|
64
|
+
|
|
65
|
+
const tools = frontmatter.tools
|
|
66
|
+
?.split(",")
|
|
67
|
+
.map((t: string) => t.trim())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
|
|
70
|
+
agents.push({
|
|
71
|
+
name: frontmatter.name,
|
|
72
|
+
description: frontmatter.description,
|
|
73
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
74
|
+
model: frontmatter.model,
|
|
75
|
+
systemPrompt: body,
|
|
76
|
+
source,
|
|
77
|
+
filePath,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return agents;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isDirectory(p: string): boolean {
|
|
85
|
+
try {
|
|
86
|
+
return fs.statSync(p).isDirectory();
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeSearchStart(cwd: string): string {
|
|
93
|
+
const resolved = realpathIfPossible(path.resolve(cwd));
|
|
94
|
+
return isDirectory(resolved) ? resolved : path.dirname(resolved);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
98
|
+
let currentDir = normalizeSearchStart(cwd);
|
|
99
|
+
while (true) {
|
|
100
|
+
const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
101
|
+
if (isDirectory(candidate)) return realpathIfPossible(candidate);
|
|
102
|
+
const parentDir = path.dirname(currentDir);
|
|
103
|
+
if (parentDir === currentDir) return null;
|
|
104
|
+
currentDir = parentDir;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function findNearestProjectAgentsInfo(cwd: string): ProjectAgentsInfo | null {
|
|
109
|
+
const dir = findNearestProjectAgentsDir(cwd);
|
|
110
|
+
if (!dir) return null;
|
|
111
|
+
|
|
112
|
+
let entries: fs.Dirent[];
|
|
113
|
+
try {
|
|
114
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
115
|
+
} catch {
|
|
116
|
+
return { dir, agentNames: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const agentNames = entries
|
|
120
|
+
.filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md"))
|
|
121
|
+
.map((entry) => entry.name.replace(/\.md$/, ""))
|
|
122
|
+
.sort();
|
|
123
|
+
|
|
124
|
+
return { dir, agentNames };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
128
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
129
|
+
const bundledAgents = scope === "project" ? [] : loadAgentsFromDir(bundledAgentsDir, "user");
|
|
130
|
+
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
131
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
132
|
+
|
|
133
|
+
if (scope === "both") {
|
|
134
|
+
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
135
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
136
|
+
} else if (scope === "user") {
|
|
137
|
+
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
138
|
+
} else {
|
|
139
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function formatAgentSourceLabel(source: AgentConfig["source"] | "unknown"): string {
|
|
146
|
+
if (source === "user") return "user (bundled/package)";
|
|
147
|
+
if (source === "project") return "project";
|
|
148
|
+
return "unknown";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
|
|
152
|
+
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
153
|
+
const listed = agents.slice(0, maxItems);
|
|
154
|
+
const remaining = agents.length - listed.length;
|
|
155
|
+
return {
|
|
156
|
+
text: listed.map((a) => `${a.name} (${formatAgentSourceLabel(a.source)}): ${a.description}`).join("; "),
|
|
157
|
+
remaining,
|
|
158
|
+
};
|
|
159
|
+
}
|