docs(29): create phase plan — adapter probe, onboarding fallback, skill injection
This commit is contained in:
parent
d386f52b25
commit
fb4a2066c3
3 changed files with 549 additions and 2 deletions
|
|
@ -59,7 +59,10 @@ Plans:
|
|||
2. Creating a PM agent or Engineer agent from the predefined templates and assigning it to a Hermes runtime produces a working agent — heartbeats execute and return meaningful results
|
||||
3. Running a full GSD workflow task (create issue, assign to Engineer, execute heartbeat, mark complete) with Hermes as the sole runtime completes without errors
|
||||
4. A machine with only Nexus, Hermes, and Ollama installed (no Anthropic/OpenAI key) can complete the entire onboarding-to-first-task flow with no paywalls or missing-key errors
|
||||
**Plans**: TBD
|
||||
**Plans:** 2 plans
|
||||
Plans:
|
||||
- [ ] 29-01-PLAN.md — Adapter probe route, onboarding wizard Hermes fallback, adapter-neutral templates
|
||||
- [ ] 29-02-PLAN.md — Hermes skill injection via promptTemplate, integration tests
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -94,4 +97,4 @@ All 16 v1 requirements are mapped to exactly one phase. No orphans.
|
|||
|-------|-----------|----------------|--------|-----------|
|
||||
| 27. Hermes Adapter | v1.4 | 1/1 | Complete | 2026-04-02 |
|
||||
| 28. Ollama Integration & Agent Surface | v1.4 | 3/3 | Complete | 2026-04-02 |
|
||||
| 29. Default Provider & End-to-End | v1.4 | 0/? | Not started | - |
|
||||
| 29. Default Provider & End-to-End | v1.4 | 0/2 | In progress | - |
|
||||
|
|
|
|||
287
.planning/phases/29-default-provider/29-01-PLAN.md
Normal file
287
.planning/phases/29-default-provider/29-01-PLAN.md
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
---
|
||||
phase: 29-default-provider
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- server/src/routes/agents.ts
|
||||
- ui/src/api/agents.ts
|
||||
- ui/src/components/NexusOnboardingWizard.tsx
|
||||
- ui/src/components/NewAgentDialog.tsx
|
||||
autonomous: true
|
||||
requirements: [DFLT-01, DFLT-02]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "GET /api/adapters/hermes_local/probe returns {available: true} when Hermes CLI is installed"
|
||||
- "GET /api/adapters/hermes_local/probe returns {available: false} when Hermes CLI is absent"
|
||||
- "NexusOnboardingWizard creates agents with adapterType hermes_local when Hermes is available and no cloud provider is detected"
|
||||
- "NexusOnboardingWizard makes directory input optional when hermes_local is selected"
|
||||
- "AGENT_TEMPLATES in NewAgentDialog no longer hardcode adapterType"
|
||||
artifacts:
|
||||
- path: "server/src/routes/agents.ts"
|
||||
provides: "Board-auth adapter probe route"
|
||||
contains: "adapters/:type/probe"
|
||||
- path: "ui/src/api/agents.ts"
|
||||
provides: "probeAdapter client method"
|
||||
contains: "probeAdapter"
|
||||
- path: "ui/src/components/NexusOnboardingWizard.tsx"
|
||||
provides: "Hermes fallback in onboarding wizard"
|
||||
contains: "hermes_local"
|
||||
- path: "ui/src/components/NewAgentDialog.tsx"
|
||||
provides: "Adapter-neutral templates"
|
||||
key_links:
|
||||
- from: "ui/src/components/NexusOnboardingWizard.tsx"
|
||||
to: "/api/adapters/hermes_local/probe"
|
||||
via: "agentsApi.probeAdapter or fetch"
|
||||
pattern: "probe.*hermes_local"
|
||||
- from: "ui/src/components/NewAgentDialog.tsx"
|
||||
to: "/agents/new"
|
||||
via: "navigate with role only, no adapterType"
|
||||
pattern: "role=.*name="
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add adapter detection probe route and update NexusOnboardingWizard + NewAgentDialog to fall back to Hermes when no cloud provider is available.
|
||||
|
||||
Purpose: Users with only Hermes + Ollama installed get a working onboarding flow without needing any paid API keys.
|
||||
Output: Probe route, wizard Hermes fallback, adapter-neutral templates.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/29-default-provider/29-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs -->
|
||||
|
||||
From server/src/routes/agents.ts (line ~667):
|
||||
```typescript
|
||||
// Existing adapter routes pattern — company-scoped:
|
||||
router.get("/companies/:companyId/adapters/:type/models", async (req, res) => { ... });
|
||||
router.get("/companies/:companyId/adapters/:type/detect-model", async (req, res) => { ... });
|
||||
router.post("/companies/:companyId/adapters/:type/test-environment", ...);
|
||||
|
||||
// findServerAdapter import:
|
||||
import { findServerAdapter, listAdapterModels, detectAdapterModel } from "../adapters/index.js";
|
||||
```
|
||||
|
||||
From ui/src/api/agents.ts (line ~168):
|
||||
```typescript
|
||||
// Existing API client pattern:
|
||||
detectModel: (companyId: string, type: string) =>
|
||||
api.get<DetectedAdapterModel | null>(
|
||||
`/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/detect-model`,
|
||||
),
|
||||
testEnvironment: (companyId: string, type: string, data: {...}) =>
|
||||
api.post<AdapterEnvironmentTestResult>(...),
|
||||
```
|
||||
|
||||
From ui/src/components/NexusOnboardingWizard.tsx:
|
||||
```typescript
|
||||
// Lines 94-119: Three agentsApi.create calls with hardcoded adapterType: "claude_local"
|
||||
// Line 80: const adapterConfig = { cwd: rootDir.trim() };
|
||||
// Line 41: effectiveOnboardingOpen controls wizard visibility
|
||||
```
|
||||
|
||||
From ui/src/components/NewAgentDialog.tsx (lines 97-99):
|
||||
```typescript
|
||||
const AGENT_TEMPLATES = [
|
||||
{ id: "pm", label: "Project Manager", role: "pm" as const, adapterType: "claude_local" as const },
|
||||
{ id: "engineer", label: "Engineer", role: "engineer" as const, adapterType: "claude_local" as const },
|
||||
];
|
||||
// Line 140: navigate with adapterType in URL
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add board-auth adapter probe route and frontend client</name>
|
||||
<read_first>
|
||||
- server/src/routes/agents.ts (lines 658-718 for existing adapter route patterns, line 186 for board auth pattern)
|
||||
- ui/src/api/agents.ts (lines 168-180 for existing adapter API methods)
|
||||
- server/src/adapters/index.ts (findServerAdapter export)
|
||||
</read_first>
|
||||
<files>server/src/routes/agents.ts, ui/src/api/agents.ts</files>
|
||||
<action>
|
||||
1. In server/src/routes/agents.ts, add a new route BEFORE the existing company-scoped adapter routes (before line 667). This route has NO companyId — it uses board auth only:
|
||||
|
||||
```typescript
|
||||
router.get("/adapters/:type/probe", async (req, res) => {
|
||||
if (req.actor.type !== "board") {
|
||||
res.status(403).json({ error: "Board authentication required" });
|
||||
return;
|
||||
}
|
||||
const type = req.params.type as string;
|
||||
const adapter = findServerAdapter(type);
|
||||
if (!adapter?.testEnvironment) {
|
||||
res.json({ available: false, status: "unknown" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await adapter.testEnvironment({
|
||||
companyId: "",
|
||||
adapterType: type,
|
||||
config: {},
|
||||
});
|
||||
const hasCliNotFound = result.checks.some(
|
||||
(c: { level: string; code?: string }) =>
|
||||
c.level === "error" && (c.code?.includes("not_found") || c.code?.includes("cli"))
|
||||
);
|
||||
res.json({ available: !hasCliNotFound, status: result.status, checks: result.checks });
|
||||
} catch {
|
||||
res.json({ available: false, status: "error" });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
2. In ui/src/api/agents.ts, add a `probeAdapter` method to the agentsApi object. It does NOT require companyId:
|
||||
|
||||
```typescript
|
||||
probeAdapter: (type: string) =>
|
||||
api.get<{ available: boolean; status: string }>(`/adapters/${encodeURIComponent(type)}/probe`),
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus && pnpm --filter server exec tsc --noEmit 2>&1 | head -20 && pnpm --filter ui exec tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q 'adapters/:type/probe' server/src/routes/agents.ts
|
||||
- grep -q 'probeAdapter' ui/src/api/agents.ts
|
||||
- grep -q 'req.actor.type !== "board"' server/src/routes/agents.ts (within the probe handler context)
|
||||
</acceptance_criteria>
|
||||
<done>Board-auth probe route returns {available, status} without companyId. Frontend client can call agentsApi.probeAdapter("hermes_local").</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Update NexusOnboardingWizard to fall back to Hermes and make NewAgentDialog templates adapter-neutral</name>
|
||||
<read_first>
|
||||
- ui/src/components/NexusOnboardingWizard.tsx (full file — 228 lines)
|
||||
- ui/src/components/NewAgentDialog.tsx (lines 90-145 for templates and handleTemplateSelect)
|
||||
</read_first>
|
||||
<files>ui/src/components/NexusOnboardingWizard.tsx, ui/src/components/NewAgentDialog.tsx</files>
|
||||
<action>
|
||||
**NexusOnboardingWizard.tsx changes (DFLT-01):**
|
||||
|
||||
1. Add import for agentsApi (already imported on line 14: `import { agentsApi } from "../api/agents"`).
|
||||
|
||||
2. Add state for adapter detection after existing state declarations (around line 50):
|
||||
```typescript
|
||||
const [defaultAdapter, setDefaultAdapter] = useState<"claude_local" | "hermes_local">("claude_local");
|
||||
const [probing, setProbing] = useState(false);
|
||||
```
|
||||
|
||||
3. Add a useEffect that probes for Hermes when the wizard opens (after the existing useEffects, around line 60):
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
if (!effectiveOnboardingOpen) return;
|
||||
setProbing(true);
|
||||
agentsApi.probeAdapter("hermes_local")
|
||||
.then((data) => {
|
||||
if (data.available) setDefaultAdapter("hermes_local");
|
||||
})
|
||||
.catch(() => {}) // graceful — keep claude_local
|
||||
.finally(() => setProbing(false));
|
||||
}, [effectiveOnboardingOpen]);
|
||||
```
|
||||
|
||||
4. In handleSubmit (line 67), replace the hardcoded adapterConfig and adapterType:
|
||||
- Change `const adapterConfig = { cwd: rootDir.trim() };` to:
|
||||
```typescript
|
||||
const adapterConfig = defaultAdapter === "hermes_local"
|
||||
? (rootDir.trim() ? { cwd: rootDir.trim() } : {})
|
||||
: { cwd: rootDir.trim() };
|
||||
```
|
||||
- Replace all three `adapterType: "claude_local"` (lines 97, 106, 115) with `adapterType: defaultAdapter`.
|
||||
|
||||
5. Update the form validation: the submit button disable condition (line 192) currently requires `!rootDir.trim()`. Change to:
|
||||
```typescript
|
||||
disabled={loading || probing || (defaultAdapter === "claude_local" && !rootDir.trim())}
|
||||
```
|
||||
|
||||
6. Update the directory input label and required state. When hermes_local is selected, the directory is optional. Change the label (line 168):
|
||||
```typescript
|
||||
<label htmlFor="nexus-root-dir" className="text-sm font-medium leading-none">
|
||||
Project root directory{defaultAdapter === "hermes_local" ? " (optional)" : ""}
|
||||
</label>
|
||||
```
|
||||
|
||||
7. Update the description text (line 157) to mention local AI when hermes is detected:
|
||||
```typescript
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{defaultAdapter === "hermes_local"
|
||||
? `${VOCAB.appName} will set up a local AI workspace with a ${VOCAB.ceo.toLowerCase()}, engineer, and generalist — no API key needed.`
|
||||
: `Choose a project root directory. ${VOCAB.appName} will set up a ${VOCAB.ceo.toLowerCase()}, engineer, and generalist to start working.`}
|
||||
</p>
|
||||
```
|
||||
|
||||
**NewAgentDialog.tsx changes (DFLT-02):**
|
||||
|
||||
1. Remove `adapterType` from AGENT_TEMPLATES. Change lines 97-99:
|
||||
```typescript
|
||||
const AGENT_TEMPLATES = [
|
||||
{ id: "pm", label: "Project Manager", role: "pm" as const },
|
||||
{ id: "engineer", label: "Engineer", role: "engineer" as const },
|
||||
];
|
||||
```
|
||||
|
||||
2. Update handleTemplateSelect (around line 136). Remove adapterType from URL:
|
||||
```typescript
|
||||
function handleTemplateSelect(template: typeof AGENT_TEMPLATES[number]) {
|
||||
closeNewAgent();
|
||||
setShowAdvancedCards(false);
|
||||
navigate(
|
||||
`/agents/new?role=${encodeURIComponent(template.role)}&name=${encodeURIComponent(template.label)}`,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This makes templates adapter-neutral. The /agents/new form will use its own default adapter logic.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus && pnpm --filter ui exec tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q 'hermes_local' ui/src/components/NexusOnboardingWizard.tsx
|
||||
- grep -q 'probeAdapter' ui/src/components/NexusOnboardingWizard.tsx
|
||||
- grep -q 'defaultAdapter' ui/src/components/NexusOnboardingWizard.tsx
|
||||
- grep -v 'adapterType' ui/src/components/NewAgentDialog.tsx | grep -q 'AGENT_TEMPLATES' (templates no longer have adapterType field)
|
||||
- ! grep -q 'claude_local' ui/src/components/NewAgentDialog.tsx (no hardcoded claude_local in templates)
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
- NexusOnboardingWizard probes for Hermes on open and uses hermes_local when available
|
||||
- Directory input is optional when hermes_local is selected
|
||||
- All three agents created with the detected adapter type
|
||||
- NewAgentDialog templates no longer hardcode adapterType — navigate with role+name only
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- TypeScript compiles for both server and ui packages
|
||||
- NexusOnboardingWizard contains "hermes_local" and "probeAdapter"
|
||||
- NewAgentDialog AGENT_TEMPLATES have no adapterType field
|
||||
- Probe route exists in agents.ts with board auth guard
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Probe route responds to GET /api/adapters/:type/probe with board auth
|
||||
- Wizard auto-detects Hermes and pre-selects hermes_local when available
|
||||
- Templates navigate without hardcoded adapter type
|
||||
- TypeScript compiles cleanly
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/29-default-provider/29-01-SUMMARY.md`
|
||||
</output>
|
||||
257
.planning/phases/29-default-provider/29-02-PLAN.md
Normal file
257
.planning/phases/29-default-provider/29-02-PLAN.md
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
---
|
||||
phase: 29-default-provider
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["29-01"]
|
||||
files_modified:
|
||||
- ui/src/components/NexusOnboardingWizard.tsx
|
||||
- server/src/__tests__/29-default-provider.test.ts
|
||||
autonomous: true
|
||||
requirements: [DFLT-03, DFLT-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A Hermes agent created via the wizard has a promptTemplate containing Nexus skill bundle content (HEARTBEAT.md, TOOLS.md)"
|
||||
- "The skill bundle is role-specific — a PM agent gets ceo/ bundle, an Engineer gets engineer/ bundle"
|
||||
- "The probe route returns available true/false based on adapter testEnvironment result"
|
||||
- "Agent creation with hermes_local and promptTemplate produces a valid agent record"
|
||||
artifacts:
|
||||
- path: "ui/src/components/NexusOnboardingWizard.tsx"
|
||||
provides: "Hermes promptTemplate with skill injection"
|
||||
contains: "promptTemplate"
|
||||
- path: "server/src/__tests__/29-default-provider.test.ts"
|
||||
provides: "Probe route + wizard flow validation tests"
|
||||
contains: "29-default-provider"
|
||||
key_links:
|
||||
- from: "ui/src/components/NexusOnboardingWizard.tsx"
|
||||
to: "server/src/routes/agents.ts"
|
||||
via: "agentsApi.create with promptTemplate in adapterConfig"
|
||||
pattern: "promptTemplate"
|
||||
- from: "server/src/routes/agents.ts"
|
||||
to: "server/src/services/default-agent-instructions.ts"
|
||||
via: "loadDefaultAgentInstructionsBundle resolves promptTemplate to skill files"
|
||||
pattern: "loadDefaultAgentInstructionsBundle"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Inject Nexus skill bundles into Hermes agent promptTemplate so GSD workflows execute correctly, and add integration tests validating the full probe-to-agent-creation flow.
|
||||
|
||||
Purpose: Without skill injection, Hermes agents created via the wizard would not follow the Nexus heartbeat loop (HEARTBEAT.md, TOOLS.md). This plan closes the skill injection gap (DFLT-03) and validates the end-to-end flow (DFLT-04).
|
||||
Output: promptTemplate injection in wizard, integration test file.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/29-default-provider/29-RESEARCH.md
|
||||
@.planning/phases/29-default-provider/29-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01 — probe route and wizard changes -->
|
||||
|
||||
From server/src/routes/agents.ts (Plan 01 addition):
|
||||
```typescript
|
||||
router.get("/adapters/:type/probe", async (req, res) => {
|
||||
// Board auth only, returns { available: boolean, status: string, checks?: ... }
|
||||
});
|
||||
```
|
||||
|
||||
From ui/src/api/agents.ts (Plan 01 addition):
|
||||
```typescript
|
||||
probeAdapter: (type: string) =>
|
||||
api.get<{ available: boolean; status: string }>(`/adapters/${encodeURIComponent(type)}/probe`),
|
||||
```
|
||||
|
||||
From server/src/services/default-agent-instructions.ts:
|
||||
```typescript
|
||||
export async function loadDefaultAgentInstructionsBundle(
|
||||
role: DefaultAgentBundleRole
|
||||
): Promise<Record<string, string>>;
|
||||
// Returns { "AGENTS.md": "...", "HEARTBEAT.md": "...", "SOUL.md": "...", "TOOLS.md": "..." }
|
||||
|
||||
export function resolveDefaultAgentInstructionsBundleRole(role: string): DefaultAgentBundleRole;
|
||||
// "ceo" => "ceo", "engineer" => "engineer", "general" => "general", else => "default"
|
||||
```
|
||||
|
||||
From server/src/routes/agents.ts (lines 458-484) — agent creation skill injection:
|
||||
```typescript
|
||||
// When agent has no explicit instructions bundle and promptTemplate is non-empty:
|
||||
// promptTemplate content goes to { "AGENTS.md": promptTemplate }
|
||||
// Then materializeManagedBundle writes the files and updates adapterConfig
|
||||
// Crucially: delete nextAdapterConfig.promptTemplate after materialization
|
||||
```
|
||||
|
||||
From ui/src/components/NexusOnboardingWizard.tsx (Plan 01 state):
|
||||
```typescript
|
||||
const [defaultAdapter, setDefaultAdapter] = useState<"claude_local" | "hermes_local">("claude_local");
|
||||
// handleSubmit creates 3 agents with adapterType: defaultAdapter
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add Hermes promptTemplate skill injection to NexusOnboardingWizard</name>
|
||||
<read_first>
|
||||
- ui/src/components/NexusOnboardingWizard.tsx (full file — post Plan 01 version)
|
||||
- server/src/services/default-agent-instructions.ts (full file, 34 lines)
|
||||
- server/src/routes/agents.ts (lines 455-485 for promptTemplate handling in ensureDefaultInstructionsBundle)
|
||||
- server/src/onboarding-assets/ceo/HEARTBEAT.md (to understand content structure)
|
||||
</read_first>
|
||||
<files>ui/src/components/NexusOnboardingWizard.tsx</files>
|
||||
<action>
|
||||
The server-side agent creation flow in agents.ts already handles promptTemplate injection:
|
||||
- If adapterConfig.promptTemplate is a non-empty string, it wraps it as {"AGENTS.md": promptTemplate} and materializes it
|
||||
- This means the wizard just needs to pass a promptTemplate containing the Nexus skill content
|
||||
|
||||
For Hermes agents, the wizard should pass a `promptTemplate` field in adapterConfig that contains the Hermes-specific preamble + instructions for the Nexus heartbeat workflow. The server's `ensureDefaultInstructionsBundle` will then handle materialization.
|
||||
|
||||
**However**, looking at the server code more carefully: when `promptTemplate` is non-empty, it ONLY creates `AGENTS.md` from it — it does NOT load HEARTBEAT.md, TOOLS.md, SOUL.md. When `promptTemplate` is empty/absent, it loads the full bundle via `loadDefaultAgentInstructionsBundle`.
|
||||
|
||||
**Key insight:** For Hermes agents, we should NOT set promptTemplate in adapterConfig. Instead, we should let the server's default path handle it — `loadDefaultAgentInstructionsBundle` will load the full role-specific bundle (AGENTS.md + HEARTBEAT.md + SOUL.md + TOOLS.md) and materialize all files. The Hermes adapter's `execute.ts` does NOT read these files, but the heartbeat service's `ctx.context.skills` mechanism already populates them from the DB — the skill content is served to the agent via the `/api/agents/me` endpoint which the HEARTBEAT.md workflow instructs the agent to call.
|
||||
|
||||
**Therefore:** The wizard does NOT need to inject a custom promptTemplate. The existing server-side flow already works for Hermes:
|
||||
1. Agent created with no promptTemplate => server loads full bundle via `loadDefaultAgentInstructionsBundle`
|
||||
2. Bundle materialized as managed files in the DB
|
||||
3. When heartbeat runs, `ctx.context.skills` contains the skill content
|
||||
4. The HEARTBEAT.md content instructs the agent to call `GET /api/agents/me` to get its instructions
|
||||
5. The Hermes adapter has `supportsLocalAgentJwt: true` so the PAPERCLIP_API_KEY is injected automatically
|
||||
|
||||
The one addition needed: a Hermes-specific system prompt that tells the agent to follow the Nexus workflow. The Hermes DEFAULT_PROMPT_TEMPLATE already handles task assignment and API calls — but it may not include the "consult your managed instructions" step.
|
||||
|
||||
**Approach:** Add a `promptTemplate` in adapterConfig ONLY for hermes_local agents that contains a system-level instruction to follow the Nexus heartbeat workflow. This goes through the server's promptTemplate path, creating an AGENTS.md that supplements the Hermes default prompt:
|
||||
|
||||
```typescript
|
||||
// In handleSubmit, after the adapterConfig line, before the create calls:
|
||||
const hermesPromptTemplate = [
|
||||
`You are "{{agentName}}", an AI agent managed by ${VOCAB.appName}.`,
|
||||
"",
|
||||
"Your identity:",
|
||||
" Agent ID: {{agentId}}",
|
||||
" Company ID: {{companyId}}",
|
||||
" API Base: {{paperclipApiUrl}}",
|
||||
" Run ID: {{runId}}",
|
||||
"",
|
||||
"IMPORTANT: Use the `terminal` tool with `curl` for ALL API calls.",
|
||||
'IMPORTANT: Always include `-H "X-Paperclip-Run-Id: {{runId}}"` on API calls that modify data.',
|
||||
"",
|
||||
"Before starting any task:",
|
||||
"1. Call `GET {{paperclipApiUrl}}/api/agents/me` to retrieve your managed instructions",
|
||||
"2. Follow the HEARTBEAT.md workflow from your instructions",
|
||||
"3. Use TOOLS.md for available API endpoints",
|
||||
"",
|
||||
"{{#taskId}}",
|
||||
"Assigned task: {{taskId}} - {{taskTitle}}",
|
||||
"{{/taskId}}",
|
||||
].join("\n");
|
||||
|
||||
// Then for each create call, when defaultAdapter === "hermes_local":
|
||||
const finalAdapterConfig = defaultAdapter === "hermes_local"
|
||||
? { ...adapterConfig, promptTemplate: hermesPromptTemplate, persistSession: true }
|
||||
: adapterConfig;
|
||||
|
||||
// Pass finalAdapterConfig instead of adapterConfig in all three agentsApi.create calls
|
||||
```
|
||||
|
||||
Make sure the Generalist agent's metadata `pendingSkillGroups: ["Creative"]` is preserved regardless of adapter type (line 118).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus && pnpm --filter ui exec tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q 'promptTemplate' ui/src/components/NexusOnboardingWizard.tsx
|
||||
- grep -q 'persistSession' ui/src/components/NexusOnboardingWizard.tsx
|
||||
- grep -q 'HEARTBEAT' ui/src/components/NexusOnboardingWizard.tsx (references heartbeat workflow in the prompt)
|
||||
- grep -q 'agents/me' ui/src/components/NexusOnboardingWizard.tsx (instructs agent to fetch its instructions)
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
- Hermes agents created via wizard get a promptTemplate that instructs the agent to follow the Nexus heartbeat workflow
|
||||
- promptTemplate includes Mustache variables (agentName, agentId, companyId, paperclipApiUrl, runId, taskId, taskTitle)
|
||||
- persistSession: true is set for Hermes agents
|
||||
- Claude_local agents are unaffected (no promptTemplate, same as before)
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add integration tests for probe route, wizard agent creation, and end-to-end flow</name>
|
||||
<read_first>
|
||||
- server/src/__tests__/heartbeat-workspace-session.test.ts (first 50 lines — for in-memory DB setup pattern)
|
||||
- server/src/__tests__/adapter-session-codecs.test.ts (first 30 lines — for test setup pattern)
|
||||
- server/src/routes/agents.ts (lines 1-20 for router setup, line 667 area for probe route from Plan 01)
|
||||
- server/src/adapters/index.ts (findServerAdapter signature)
|
||||
</read_first>
|
||||
<files>server/src/__tests__/29-default-provider.test.ts</files>
|
||||
<action>
|
||||
Create a new test file at `server/src/__tests__/29-default-provider.test.ts` with unit tests covering:
|
||||
|
||||
**Test group 1: Adapter probe route logic**
|
||||
These test the probe logic directly (no HTTP — test the route handler's logic):
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
```
|
||||
|
||||
Since testing the Express route directly is complex, test the adapter probe logic by calling `findServerAdapter` and `testEnvironment` directly:
|
||||
|
||||
1. `it("findServerAdapter returns hermes_local adapter with testEnvironment")` — verify the adapter exists and has testEnvironment function
|
||||
2. `it("hermes testEnvironment handles missing CLI gracefully")` — if hermes is not installed in CI, verify the result contains a check with level "error" and a code containing "not_found" or "cli"
|
||||
|
||||
**Test group 2: Hermes promptTemplate construction**
|
||||
Extract the promptTemplate construction logic from the wizard into a testable helper OR test the expected string content:
|
||||
|
||||
3. `it("hermes promptTemplate contains required Mustache variables")` — build the template string and verify it contains `{{agentName}}`, `{{agentId}}`, `{{companyId}}`, `{{paperclipApiUrl}}`, `{{runId}}`, `{{taskId}}`, `{{taskTitle}}`
|
||||
4. `it("hermes promptTemplate instructs agent to call /api/agents/me")` — verify the template contains `agents/me`
|
||||
5. `it("hermes promptTemplate mentions HEARTBEAT.md workflow")` — verify the template references the heartbeat workflow
|
||||
|
||||
**Test group 3: Default agent instructions bundle (DFLT-03)**
|
||||
6. `it("loadDefaultAgentInstructionsBundle loads ceo bundle with all 4 files")` — call with "ceo", verify keys include AGENTS.md, HEARTBEAT.md, SOUL.md, TOOLS.md
|
||||
7. `it("loadDefaultAgentInstructionsBundle loads engineer bundle with all 4 files")` — same for "engineer"
|
||||
8. `it("resolveDefaultAgentInstructionsBundleRole maps known roles correctly")` — verify ceo->ceo, engineer->engineer, general->general, unknown->default
|
||||
|
||||
Import `loadDefaultAgentInstructionsBundle` and `resolveDefaultAgentInstructionsBundleRole` from `../services/default-agent-instructions.js`.
|
||||
|
||||
For test group 2, define the promptTemplate string directly in the test file (duplicated from the wizard) to verify its content. This is intentional — the test validates the contract, not the implementation.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus && pnpm --filter server test --run -- 29-default-provider 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- test file exists at server/src/__tests__/29-default-provider.test.ts
|
||||
- grep -q 'loadDefaultAgentInstructionsBundle' server/src/__tests__/29-default-provider.test.ts
|
||||
- grep -q 'HEARTBEAT.md' server/src/__tests__/29-default-provider.test.ts
|
||||
- grep -q 'promptTemplate' server/src/__tests__/29-default-provider.test.ts
|
||||
- pnpm --filter server test --run -- 29-default-provider exits 0
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
- All tests pass: probe logic, promptTemplate content validation, default bundle loading
|
||||
- Tests verify the contract that Hermes agents get skill content via the standard bundle path
|
||||
- Tests confirm promptTemplate contains all required Mustache variables for Hermes adapter
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter server test --run -- 29-default-provider` passes all tests
|
||||
- `pnpm --filter ui exec tsc --noEmit` compiles cleanly
|
||||
- NexusOnboardingWizard contains promptTemplate injection for hermes_local
|
||||
- Test file covers probe, promptTemplate, and bundle loading
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Hermes agents created via wizard get a promptTemplate that enables the Nexus GSD workflow
|
||||
- Integration tests validate the probe route logic and skill bundle loading
|
||||
- A machine with only Hermes + Ollama can complete onboarding and get working agents (no paywalls)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/29-default-provider/29-02-SUMMARY.md`
|
||||
</output>
|
||||
Loading…
Add table
Reference in a new issue