niahere 0.2.63 → 0.2.64

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.63",
3
+ "version": "0.2.64",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,75 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { getEmployee, getEmployeeDir } from "../core/employees";
4
+ import { ONBOARDING_INSTRUCTIONS } from "../core/employees";
5
+ import { getEnvironmentPrompt, getModePrompt } from "../prompts";
6
+ import { getSkillsSummary } from "../core/skills";
7
+ import { getAgentsSummary } from "../core/agents";
8
+
9
+ function loadFile(dir: string, name: string): string {
10
+ const filePath = join(dir, name);
11
+ if (!existsSync(filePath)) return "";
12
+ return readFileSync(filePath, "utf8").trim();
13
+ }
14
+
15
+ export function buildEmployeePrompt(name: string): string {
16
+ const employee = getEmployee(name);
17
+ if (!employee) return "";
18
+
19
+ const dir = getEmployeeDir(name);
20
+ const parts: string[] = [];
21
+
22
+ // Core identity (the EMPLOYEE.md body)
23
+ if (employee.body) parts.push(employee.body);
24
+
25
+ // Environment + mode + capabilities
26
+ parts.push(getEnvironmentPrompt());
27
+
28
+ const modePrompt = getModePrompt("chat");
29
+ if (modePrompt) parts.push(modePrompt);
30
+
31
+ const skills = getSkillsSummary();
32
+ if (skills) parts.push(skills);
33
+
34
+ const agents = getAgentsSummary();
35
+ if (agents) parts.push(agents);
36
+
37
+ // Onboarding instructions (only when status=onboarding)
38
+ if (employee.status === "onboarding") {
39
+ parts.push(ONBOARDING_INSTRUCTIONS);
40
+ }
41
+
42
+ // Employee metadata context
43
+ parts.push(`## Your Profile
44
+ - **Name:** ${employee.name}
45
+ - **Role:** ${employee.role}
46
+ - **Project:** ${employee.project}
47
+ - **Repo:** ${employee.repo}
48
+ - **Status:** ${employee.status}
49
+ - **Max Sub-Employees:** ${employee.maxSubEmployees}`);
50
+
51
+ // State files
52
+ const goals = loadFile(dir, "goals.md");
53
+ if (goals) parts.push(`## Your Current Goals\n${goals}`);
54
+
55
+ const memory = loadFile(dir, "memory.md");
56
+ if (memory) parts.push(`## Your Memory\n${memory}`);
57
+
58
+ const decisions = loadFile(dir, "decisions.md");
59
+ if (decisions) parts.push(`## Decision Log\n${decisions}`);
60
+
61
+ const org = loadFile(dir, "org.md");
62
+ if (org) parts.push(`## Your Organization\n${org}`);
63
+
64
+ // Onboarding context
65
+ const brief = loadFile(join(dir, "onboarding"), "brief.md");
66
+ if (brief) parts.push(`## Onboarding Brief\n${brief}`);
67
+
68
+ const discovery = loadFile(join(dir, "onboarding"), "discovery.md");
69
+ if (discovery) parts.push(`## Self-Discovery Notes\n${discovery}`);
70
+
71
+ const plan = loadFile(join(dir, "onboarding"), "plan.md");
72
+ if (plan) parts.push(`## Initial Plan\n${plan}`);
73
+
74
+ return parts.join("\n\n");
75
+ }
@@ -6,8 +6,10 @@ import { join } from "path";
6
6
  import { homedir } from "os";
7
7
  import { randomUUID } from "crypto";
8
8
  import { buildSystemPrompt, getSessionContext } from "./identity";
9
- import { getAgentDefinitions } from "../core/agents";
10
- import { Session, Message, ActiveEngine } from "../db/models";
9
+ import { buildEmployeePrompt } from "./employee-prompt";
10
+ import { getEmployee } from "../core/employees";
11
+ import { getAgentDefinitions, scanAgents } from "../core/agents";
12
+ import { Session, Message, ActiveEngine, Job } from "../db/models";
11
13
  import type {
12
14
  Attachment,
13
15
  SendResult,
@@ -134,7 +136,36 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
134
136
  systemPrompt += "\n\n" + sessionContext;
135
137
  }
136
138
 
137
- const cwd = homedir();
139
+ // Context overrides: employee > agent > job > default
140
+ let cwd = homedir();
141
+ if (opts.employee) {
142
+ const empPrompt = buildEmployeePrompt(opts.employee);
143
+ if (empPrompt) systemPrompt = empPrompt;
144
+ const emp = getEmployee(opts.employee);
145
+ if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
146
+ } else if (opts.agent) {
147
+ const agents = scanAgents();
148
+ const agentDef = agents.find((a) => a.name === opts.agent);
149
+ if (agentDef) systemPrompt = agentDef.body;
150
+ } else if (opts.job) {
151
+ // Job chat: load job and use its context
152
+ const jobData = await Job.get(opts.job);
153
+ if (jobData) {
154
+ // If job has an employee, use employee prompt
155
+ if (jobData.employee) {
156
+ const empPrompt = buildEmployeePrompt(jobData.employee);
157
+ if (empPrompt) systemPrompt = empPrompt;
158
+ const emp = getEmployee(jobData.employee);
159
+ if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
160
+ } else if (jobData.agent) {
161
+ // If job has an agent, use agent prompt
162
+ const agents = scanAgents();
163
+ const agentDef = agents.find((a) => a.name === jobData.agent);
164
+ if (agentDef) systemPrompt = agentDef.body;
165
+ }
166
+ systemPrompt += `\n\n## Job Context\nYou are chatting in the context of job "${jobData.name}" (schedule: ${jobData.schedule}).\n\nJob prompt:\n${jobData.prompt}`;
167
+ }
168
+ }
138
169
 
139
170
  let sessionId: string | null = null;
140
171
  if (typeof resume === "string") {
@@ -493,15 +524,17 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
493
524
  });
494
525
  },
495
526
 
496
- close() {
527
+ async close() {
497
528
  // Enqueue finalization — processed by daemon or inline if we are the daemon
498
529
  if (sessionId && messageCount > 0 && !pending) {
499
- finalizeSession(sessionId, room).catch((err) => {
530
+ try {
531
+ await finalizeSession(sessionId, room);
532
+ } catch (err) {
500
533
  log.error({ err, room }, "finalization enqueue failed during close");
501
- });
534
+ }
502
535
  }
503
536
  teardown();
504
- ActiveEngine.unregister(room).catch(() => {});
537
+ await ActiveEngine.unregister(room).catch(() => {});
505
538
  },
506
539
  };
507
540
  }
@@ -4,6 +4,7 @@ import { getPaths } from "../utils/paths";
4
4
  import { getEnvironmentPrompt, getModePrompt, getChannelPrompt } from "../prompts";
5
5
  import { getSkillsSummary } from "../core/skills";
6
6
  import { getAgentsSummary } from "../core/agents";
7
+ import { getEmployeesSummary } from "../core/employees";
7
8
  import { Session } from "../db/models";
8
9
  import type { Mode } from "../types";
9
10
 
@@ -19,7 +20,10 @@ function loadFile(dir: string, name: string): string {
19
20
  export function loadIdentity(): string {
20
21
  const { selfDir } = getPaths();
21
22
  const files = ["identity.md", "owner.md", "soul.md", "rules.md", "memory.md"];
22
- return files.map((f) => loadFile(selfDir, f)).filter(Boolean).join("\n\n");
23
+ return files
24
+ .map((f) => loadFile(selfDir, f))
25
+ .filter(Boolean)
26
+ .join("\n\n");
23
27
  }
24
28
 
25
29
  export function buildSystemPrompt(mode: Mode = "chat", channel: string = "terminal"): string {
@@ -42,6 +46,9 @@ export function buildSystemPrompt(mode: Mode = "chat", channel: string = "termin
42
46
  const agents = getAgentsSummary();
43
47
  if (agents) parts.push(agents);
44
48
 
49
+ const employees = getEmployeesSummary();
50
+ if (employees) parts.push(employees);
51
+
45
52
  return parts.join("\n\n");
46
53
  }
47
54
 
package/src/chat/repl.ts CHANGED
@@ -103,7 +103,18 @@ async function pickSession(): Promise<string | null> {
103
103
 
104
104
  export type ChatMode = "continue" | "new" | "pick";
105
105
 
106
- export async function startRepl(mode: ChatMode = "continue", simulateChannel?: string): Promise<void> {
106
+ export interface ReplContext {
107
+ employee?: string;
108
+ agent?: string;
109
+ job?: string;
110
+ initialMessage?: string;
111
+ }
112
+
113
+ export async function startRepl(
114
+ mode: ChatMode = "continue",
115
+ simulateChannel?: string,
116
+ context?: ReplContext,
117
+ ): Promise<void> {
107
118
  try {
108
119
  await runMigrations();
109
120
  } catch (err) {
@@ -131,13 +142,24 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
131
142
  }
132
143
 
133
144
  const channel = simulateChannel || "terminal";
134
- const engine = await createChatEngine({ room: "terminal", channel, resume, mcpServers: getMcpServers() });
145
+ const contextLabel = context?.employee || context?.agent || context?.job;
146
+ const room = contextLabel ? `chat-${contextLabel}` : "terminal";
147
+ const engine = await createChatEngine({
148
+ room,
149
+ channel,
150
+ resume,
151
+ mcpServers: getMcpServers(),
152
+ employee: context?.employee,
153
+ agent: context?.agent,
154
+ job: context?.job,
155
+ });
135
156
 
136
157
  // Welcome
137
158
  const isResumed = engine.sessionId && resume;
138
159
  const sessionNote = isResumed ? "resumed" : "new session";
139
160
  const channelNote = simulateChannel ? ` as ${simulateChannel}` : "";
140
- console.log(`\n${DIM}nia chat${channelNote}${RESET} ${DIM}(${sessionNote})${RESET}`);
161
+ const contextNote = contextLabel ? ` as ${contextLabel}` : "";
162
+ console.log(`\n${DIM}nia chat${contextNote}${channelNote}${RESET} ${DIM}(${sessionNote})${RESET}`);
141
163
  console.log(`${DIM}type /exit to quit${RESET}\n`);
142
164
 
143
165
  const rl = readline.createInterface({
@@ -146,22 +168,7 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
146
168
  prompt: `${BOLD}>${RESET} `,
147
169
  });
148
170
 
149
- rl.prompt();
150
-
151
- rl.on("line", async (line: string) => {
152
- const input = line.trim();
153
-
154
- if (!input) {
155
- rl.prompt();
156
- return;
157
- }
158
-
159
- const exitCommands = ["/exit", "/quit", ".exit", ".quit", "exit", "quit"];
160
- if (exitCommands.includes(input.toLowerCase())) {
161
- rl.close();
162
- return;
163
- }
164
-
171
+ async function sendAndDisplay(input: string): Promise<void> {
165
172
  const status = new StatusLine();
166
173
  status.start("thinking");
167
174
 
@@ -171,7 +178,6 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
171
178
  try {
172
179
  const { result, costUsd, turns } = await engine.send(input, {
173
180
  onStream(textSoFar) {
174
- // Stream response text as it arrives
175
181
  if (!responseStarted) {
176
182
  status.stop();
177
183
  process.stdout.write("\n");
@@ -190,12 +196,10 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
190
196
  },
191
197
  });
192
198
 
193
- // If streaming didn't fire (e.g. tool-only turns), print the result
194
199
  if (!responseStarted && result.trim()) {
195
200
  status.stop();
196
201
  process.stdout.write(`\n${result.trim()}`);
197
202
  } else if (responseStarted) {
198
- // Print any remaining text that wasn't streamed
199
203
  const remaining = result.slice(streamedLength);
200
204
  if (remaining.trim()) {
201
205
  process.stdout.write(remaining);
@@ -204,7 +208,6 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
204
208
  status.stop();
205
209
  }
206
210
 
207
- // Cost line
208
211
  const costStr = costUsd > 0 ? `$${costUsd.toFixed(4)}` : "";
209
212
  const turnsStr = turns > 0 ? `${turns} turn${turns !== 1 ? "s" : ""}` : "";
210
213
  const meta = [costStr, turnsStr].filter(Boolean).join(" · ");
@@ -218,13 +221,37 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
218
221
  const msg = err instanceof Error ? err.message : String(err);
219
222
  console.error(`\n${DIM}error:${RESET} ${msg}\n`);
220
223
  }
224
+ }
225
+
226
+ // Auto-send initial message if provided (e.g. onboarding kickoff)
227
+ if (context?.initialMessage) {
228
+ await sendAndDisplay(context.initialMessage);
229
+ }
230
+
231
+ rl.prompt();
232
+
233
+ rl.on("line", async (line: string) => {
234
+ const input = line.trim();
235
+
236
+ if (!input) {
237
+ rl.prompt();
238
+ return;
239
+ }
240
+
241
+ const exitCommands = ["/exit", "/quit", ".exit", ".quit", "exit", "quit"];
242
+ if (exitCommands.includes(input.toLowerCase())) {
243
+ rl.close();
244
+ return;
245
+ }
246
+
247
+ await sendAndDisplay(input);
221
248
 
222
249
  rl.prompt();
223
250
  });
224
251
 
225
- rl.on("close", () => {
252
+ rl.on("close", async () => {
226
253
  console.log(`\n${DIM}bye${RESET}`);
227
- engine.close();
254
+ await engine.close();
228
255
  closeDb()
229
256
  .catch(() => {})
230
257
  .finally(() => process.exit(0));
@@ -0,0 +1,124 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { getEmployee, getEmployeeDir } from "../core/employees";
4
+ import { fail, BOLD, RESET } from "../utils/cli";
5
+ import { startRepl } from "../chat/repl";
6
+
7
+ // Permanent identity — stays in EMPLOYEE.md after onboarding
8
+ const CORE_BODY = `You are {name}, a co-founder working with Aman.
9
+ {projectLine}
10
+ You operate independently but seek approval before externally visible actions.
11
+
12
+ ## Who You Are
13
+ You are NOT an assistant. You are a co-founder and thought partner. This means:
14
+ - Challenge ideas that seem weak, unfocused, or premature. Say "I don't think that's the right move because..." not "Great idea!"
15
+ - Ask hard questions: "Who actually wants this?", "What's the evidence?", "Why this over the 5 easier things?"
16
+ - Have strong opinions, loosely held. Push back, but update when presented with better reasoning.
17
+ - Be direct. No filler, no sycophancy, no "Got it!", no performative enthusiasm.
18
+ - Think critically about priorities. "We could, but should we?" is more valuable than "On it!"
19
+ - When Aman tells you something, probe it. A real co-founder doesn't just accept the brief — they stress-test it.
20
+
21
+ ## Your Authority
22
+ - Create and manage scheduled jobs scoped to your project
23
+ - Create sub-employees (up to {maxSubEmployees}) and agents under your org
24
+ - Read/write code in your project repo
25
+ - Draft content, PRs, deployments (approval required before publishing)
26
+
27
+ ## Approval Required For
28
+ - Deploying code to production
29
+ - Publishing content externally
30
+ - Creating sub-employees
31
+ - Any action visible outside the project repo
32
+ - Spending money or signing up for services
33
+
34
+ ## How You Work
35
+ - Maintain your goals.md with current priorities
36
+ - Log significant decisions in decisions.md with [pending] status when approval needed
37
+ - Update memory.md with learnings after each session
38
+ - When blocked or facing a big decision, write to decisions.md as [pending] and tell the user
39
+ - At the start of each session, review your state files and what's changed in the repo
40
+
41
+ ## State Files
42
+ You have persistent state files in your employee directory. Read and update them:
43
+ - **goals.md** — your current goals and success criteria
44
+ - **memory.md** — what you've learned, decided, observed across sessions
45
+ - **decisions.md** — decision log (mark as [pending], [approved], or [rejected])
46
+ - **org.md** — sub-employees and agents you've created`;
47
+
48
+ export async function employeeAdd(): Promise<void> {
49
+ const args = process.argv.slice(4);
50
+
51
+ const flagValue = (flag: string): string | undefined => {
52
+ const idx = args.indexOf(flag);
53
+ return idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("--") ? args[idx + 1] : undefined;
54
+ };
55
+
56
+ const nameArg = args[0] && !args[0].startsWith("--") ? args[0] : undefined;
57
+ const name = nameArg || `new-employee-${Math.random().toString(36).slice(2, 6)}`;
58
+ const project = flagValue("--project") || "";
59
+ const repoArg = flagValue("--repo") || "";
60
+ const role = flagValue("--role") || "Co-Founder";
61
+ const model = flagValue("--model") || "opus";
62
+ const maxSubs = parseInt(flagValue("--max-sub-employees") || "3", 10);
63
+
64
+ if (getEmployee(name)) fail(`Employee "${name}" already exists.`);
65
+
66
+ const repo = repoArg ? resolve(repoArg) : "";
67
+ if (repo && !existsSync(repo)) fail(`Repo path does not exist: ${repo}`);
68
+
69
+ // Scaffold directory
70
+ const empDir = getEmployeeDir(name);
71
+ mkdirSync(empDir, { recursive: true });
72
+ mkdirSync(`${empDir}/onboarding`, { recursive: true });
73
+
74
+ const projectLine = project ? `You are responsible for ${project}.` : "";
75
+
76
+ const body = CORE_BODY.replace(/\{name\}/g, name)
77
+ .replace(/\{projectLine\}\n/g, projectLine ? `${projectLine}\n` : "")
78
+ .replace(/\{maxSubEmployees\}/g, String(maxSubs));
79
+
80
+ const frontmatter = [
81
+ "---",
82
+ `name: ${name}`,
83
+ `project: "${project}"`,
84
+ `repo: "${repo}"`,
85
+ `role: ${role}`,
86
+ `model: ${model}`,
87
+ `status: onboarding`,
88
+ `maxSubEmployees: ${maxSubs}`,
89
+ `created: ${new Date().toISOString().slice(0, 10)}`,
90
+ "---",
91
+ ].join("\n");
92
+
93
+ writeFileSync(`${empDir}/EMPLOYEE.md`, `${frontmatter}\n\n${body}\n`);
94
+ writeFileSync(`${empDir}/goals.md`, "# Goals\n\n");
95
+ writeFileSync(`${empDir}/memory.md`, "# Memory\n\n");
96
+ writeFileSync(`${empDir}/decisions.md`, "# Decisions\n\n");
97
+ writeFileSync(`${empDir}/org.md`, "# Organization\n\n");
98
+ writeFileSync(`${empDir}/onboarding/brief.md`, "");
99
+ writeFileSync(`${empDir}/onboarding/discovery.md`, "");
100
+ writeFileSync(`${empDir}/onboarding/plan.md`, "");
101
+
102
+ // Build a context-aware kickoff message so the agent starts proactively
103
+ const provided: string[] = [];
104
+ const missing: string[] = [];
105
+
106
+ if (nameArg) provided.push(`name: ${nameArg}`);
107
+ else missing.push("name (placeholder assigned — suggest a real one)");
108
+
109
+ if (project) provided.push(`project: ${project}`);
110
+ else missing.push("project");
111
+
112
+ if (repo) provided.push(`repo: ${repo}`);
113
+ else missing.push("repo path");
114
+
115
+ const initialMessage = [
116
+ "New employee created. Start onboarding.",
117
+ provided.length > 0 ? `Provided: ${provided.join(", ")}.` : "Nothing provided yet.",
118
+ missing.length > 0 ? `Missing: ${missing.join(", ")}.` : "All basics provided — proceed to brief.",
119
+ ].join(" ");
120
+
121
+ console.log(`\n${BOLD}${name}${RESET} created — starting onboarding...\n`);
122
+
123
+ await startRepl("new", undefined, { employee: name, initialMessage });
124
+ }
@@ -0,0 +1,167 @@
1
+ import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
2
+ import { join } from "path";
3
+ import { scanEmployees, getEmployee, getEmployeeDir } from "../core/employees";
4
+ import { fail, DIM, RESET } from "../utils/cli";
5
+
6
+ const HELP = `Usage: nia employee <command>
7
+
8
+ Commands:
9
+ list List all employees
10
+ show <name> Show employee details and state
11
+ add <name> Create employee and start onboarding
12
+ --project <label> Project name (required)
13
+ --repo <path> Project repo path (required)
14
+ --role <role> Role title (default: "Chief of Staff")
15
+ --model <model> Model override (default: opus)
16
+ --max-sub-employees <n> Max sub-employees (default: 3)
17
+ pause <name> Pause an employee
18
+ resume <name> Resume a paused employee
19
+ remove <name> Remove an employee
20
+ approvals [name] Show pending approvals`;
21
+
22
+ export async function employeeCommand(): Promise<void> {
23
+ const subcommand = process.argv[3];
24
+
25
+ if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
26
+ console.log(HELP);
27
+ return;
28
+ }
29
+
30
+ switch (subcommand) {
31
+ case "list": {
32
+ const employees = scanEmployees();
33
+ if (employees.length === 0) {
34
+ console.log("No employees. Create one with: nia employee add <name> --project <project> --repo <path>");
35
+ } else {
36
+ for (const e of employees) {
37
+ const model = e.model ? ` (${e.model})` : "";
38
+ const parent = e.parent ? ` → ${e.parent}` : "";
39
+ console.log(` ${e.name}${model} [${e.status}]${parent}`);
40
+ console.log(` ${e.role} — ${e.project}`);
41
+ }
42
+ }
43
+ break;
44
+ }
45
+
46
+ case "show": {
47
+ const name = process.argv[4];
48
+ if (!name) fail("Usage: nia employee show <name>");
49
+ const emp = getEmployee(name);
50
+ if (!emp) fail(`Employee "${name}" not found.`);
51
+ console.log(`Name: ${emp.name}`);
52
+ console.log(`Role: ${emp.role}`);
53
+ console.log(`Project: ${emp.project}`);
54
+ console.log(`Repo: ${emp.repo}`);
55
+ console.log(`Status: ${emp.status}`);
56
+ console.log(`Model: ${emp.model || "(default)"}`);
57
+ console.log(`Created: ${emp.created}`);
58
+ if (emp.parent) console.log(`Parent: ${emp.parent}`);
59
+ console.log(`Max Subs: ${emp.maxSubEmployees}`);
60
+
61
+ const dir = getEmployeeDir(name);
62
+ const goals = loadFilePreview(join(dir, "goals.md"));
63
+ if (goals) console.log(`\n--- Goals ---\n${goals}`);
64
+ const pendingCount = loadFilePendingCount(join(dir, "decisions.md"));
65
+ if (pendingCount > 0) console.log(`\nPending approvals: ${pendingCount}`);
66
+ break;
67
+ }
68
+
69
+ case "pause": {
70
+ const name = process.argv[4];
71
+ if (!name) fail("Usage: nia employee pause <name>");
72
+ updateStatus(name, "paused");
73
+ console.log(`${name} paused.`);
74
+ break;
75
+ }
76
+
77
+ case "resume": {
78
+ const name = process.argv[4];
79
+ if (!name) fail("Usage: nia employee resume <name>");
80
+ updateStatus(name, "active");
81
+ console.log(`${name} resumed.`);
82
+ break;
83
+ }
84
+
85
+ case "remove": {
86
+ const name = process.argv[4];
87
+ if (!name) fail("Usage: nia employee remove <name>");
88
+ const emp = getEmployee(name);
89
+ if (!emp) fail(`Employee "${name}" not found.`);
90
+ const dir = getEmployeeDir(name);
91
+ rmSync(dir, { recursive: true, force: true });
92
+ console.log(`${name} removed.`);
93
+ break;
94
+ }
95
+
96
+ case "approvals": {
97
+ const name = process.argv[4];
98
+ const employees = name ? [getEmployee(name)].filter(Boolean) : scanEmployees();
99
+ if (employees.length === 0) {
100
+ console.log(name ? `Employee "${name}" not found.` : "No employees.");
101
+ return;
102
+ }
103
+ let found = false;
104
+ for (const emp of employees) {
105
+ if (!emp) continue;
106
+ const dir = getEmployeeDir(emp.name);
107
+ const decisionsFile = join(dir, "decisions.md");
108
+ if (!existsSync(decisionsFile)) continue;
109
+ const content = readFileSync(decisionsFile, "utf8");
110
+ const pending = content.split(/^## /m).filter((s) => s.includes("[pending]"));
111
+ if (pending.length > 0) {
112
+ found = true;
113
+ console.log(`\n${DIM}${emp.name}:${RESET}`);
114
+ for (const p of pending) {
115
+ console.log(` ${p.trim().split("\n")[0]}`);
116
+ }
117
+ }
118
+ }
119
+ if (!found) console.log("No pending approvals.");
120
+ break;
121
+ }
122
+
123
+ case "add": {
124
+ const { employeeAdd } = await import("./employee-add");
125
+ await employeeAdd();
126
+ break;
127
+ }
128
+
129
+ default: {
130
+ // If subcommand matches an employee name, start chat
131
+ if (subcommand) {
132
+ const emp = getEmployee(subcommand);
133
+ if (emp) {
134
+ const { startRepl } = await import("../chat/repl");
135
+ await startRepl("continue", undefined, { employee: emp.name });
136
+ break;
137
+ }
138
+ }
139
+ if (subcommand) console.error(`Unknown subcommand: ${subcommand}`);
140
+ console.log(HELP);
141
+ process.exit(subcommand ? 1 : 0);
142
+ }
143
+ }
144
+ }
145
+
146
+ function updateStatus(name: string, status: "active" | "paused"): void {
147
+ const emp = getEmployee(name);
148
+ if (!emp) fail(`Employee "${name}" not found.`);
149
+ const dir = getEmployeeDir(name);
150
+ const empFile = join(dir, "EMPLOYEE.md");
151
+ let content = readFileSync(empFile, "utf8");
152
+ content = content.replace(/^(status:\s*).+$/m, `$1${status}`);
153
+ writeFileSync(empFile, content);
154
+ }
155
+
156
+ function loadFilePreview(path: string): string {
157
+ if (!existsSync(path)) return "";
158
+ const content = readFileSync(path, "utf8").trim();
159
+ const lines = content.split("\n").slice(0, 10);
160
+ return lines.join("\n");
161
+ }
162
+
163
+ function loadFilePendingCount(path: string): number {
164
+ if (!existsSync(path)) return 0;
165
+ const content = readFileSync(path, "utf8");
166
+ return (content.match(/\[pending\]/g) || []).length;
167
+ }
package/src/cli/index.ts CHANGED
@@ -15,6 +15,7 @@ import { sendCommand, telegramCommand, slackCommand } from "./channels";
15
15
  import { rulesCommand, memoryCommand } from "./self";
16
16
  import { watchCommand } from "./watch";
17
17
  import { agentCommand } from "./agent";
18
+ import { employeeCommand } from "./employee";
18
19
 
19
20
  // Set LOG_LEVEL from config before anything else logs
20
21
  try {
@@ -215,7 +216,7 @@ switch (command) {
215
216
  if (meta) process.stderr.write(`\n${DIM}${meta}${RST}`);
216
217
  process.stdout.write("\n");
217
218
 
218
- engine.close();
219
+ await engine.close();
219
220
  });
220
221
  process.exit(0);
221
222
  } else {
@@ -307,9 +308,18 @@ switch (command) {
307
308
  : chatArgs.includes("--resume") || chatArgs.includes("-r")
308
309
  ? ("pick" as const)
309
310
  : ("new" as const);
310
- const chIdx = chatArgs.indexOf("--channel");
311
- const simChannel = chIdx !== -1 && chatArgs[chIdx + 1] ? chatArgs[chIdx + 1] : undefined;
312
- await startRepl(mode, simChannel);
311
+ const flagVal = (flag: string) => {
312
+ const idx = chatArgs.indexOf(flag);
313
+ return idx !== -1 && chatArgs[idx + 1] ? chatArgs[idx + 1] : undefined;
314
+ };
315
+ const simChannel = flagVal("--channel");
316
+ const context = {
317
+ employee: flagVal("--employee"),
318
+ agent: flagVal("--agent"),
319
+ job: flagVal("--job"),
320
+ };
321
+ const hasContext = context.employee || context.agent || context.job;
322
+ await startRepl(mode, simChannel, hasContext ? context : undefined);
313
323
  break;
314
324
  }
315
325
 
@@ -318,6 +328,11 @@ switch (command) {
318
328
  break;
319
329
  }
320
330
 
331
+ case "employee": {
332
+ await employeeCommand();
333
+ break;
334
+ }
335
+
321
336
  case "skills": {
322
337
  const { scanSkills: loadSkills } = await import("../core/skills");
323
338
  const filter = process.argv[3]; // e.g. "project", "nia", "shared", "claude"
@@ -545,7 +560,7 @@ Daemon:
545
560
  logs [-f] [--channel ch] Daemon logs (filter by channel)
546
561
 
547
562
  Chat:
548
- chat [-c] [-r] [--channel ch] Interactive chat (new session by default)
563
+ chat [-c] [-r] [--employee|--agent|--job name] Interactive chat
549
564
  run <prompt> One-shot execution
550
565
  history [room] Recent messages
551
566
  send [-c ch] <msg> Send a message via channel
@@ -557,6 +572,7 @@ Persona:
557
572
  rules [show|reset] View or reset rules.md
558
573
  memory [show|reset] View or reset memory.md
559
574
  agent <sub> List/show agents
575
+ employee <sub> Manage employees
560
576
  skills [source] List available skills
561
577
 
562
578
  Channels:
package/src/cli/job.ts CHANGED
@@ -23,6 +23,7 @@ Commands:
23
23
  --type cron|interval|once Schedule type (default: cron)
24
24
  --always Run 24/7 regardless of active hours
25
25
  --agent <name> Assign an agent to the job
26
+ --employee <name> Assign an employee to the job
26
27
  --model <model> Model override (e.g. haiku, sonnet, opus)
27
28
  --stateless yes|no Disable working memory for this job
28
29
  update <name> Update a job
@@ -32,6 +33,7 @@ Commands:
32
33
  --type cron|interval|once Change schedule type
33
34
  --always / --no-always Toggle 24/7 mode
34
35
  --agent <name> Assign agent (--no-agent to remove)
36
+ --employee <name> Assign employee (--no-employee to remove)
35
37
  --model <model> Model override (--no-model to remove)
36
38
  --stateless yes|no Toggle working memory
37
39
  remove <name> Delete a job
@@ -96,7 +98,8 @@ export async function jobCommand(): Promise<void> {
96
98
  const tag = job.always ? " always" : "";
97
99
  const type = job.scheduleType !== "cron" ? ` (${job.scheduleType})` : "";
98
100
  const agentTag = job.agent ? ` [${job.agent}]` : "";
99
- console.log(` ${job.enabled ? "●" : ""} ${job.name} ${job.schedule}${type}${tag}${agentTag}`);
101
+ const empTag = job.employee ? ` [emp:${job.employee}]` : "";
102
+ console.log(` ${job.enabled ? "●" : "○"} ${job.name} ${job.schedule}${type}${tag}${agentTag}${empTag}`);
100
103
  }
101
104
  }
102
105
  });
@@ -122,6 +125,7 @@ export async function jobCommand(): Promise<void> {
122
125
  const statelessRaw = args.getString("stateless");
123
126
  const stateless = statelessRaw ? ["yes", "y", "true", "t", "1"].includes(statelessRaw.toLowerCase()) : false;
124
127
  const agent = args.getString("agent");
128
+ const employee = args.getString("employee");
125
129
  const model = args.getString("model");
126
130
 
127
131
  const [name, schedule, ...promptParts] = args.positional;
@@ -149,7 +153,7 @@ export async function jobCommand(): Promise<void> {
149
153
  const config = getConfig();
150
154
  const nextRunAt = computeInitialNextRun(scheduleType, schedule, config.timezone);
151
155
  await withDb(async () => {
152
- await Job.create(name, schedule, prompt, always, scheduleType, nextRunAt, agent, stateless, model);
156
+ await Job.create(name, schedule, prompt, always, scheduleType, nextRunAt, agent, stateless, model, employee);
153
157
  console.log(`Job "${name}" added (${scheduleType}: ${schedule}).${always ? " (runs 24/7)" : ""}`);
154
158
  });
155
159
  } catch (err) {
@@ -213,6 +217,7 @@ export async function jobCommand(): Promise<void> {
213
217
  model: string | null;
214
218
  scheduleType: ScheduleType;
215
219
  agent: string | null;
220
+ employee: string | null;
216
221
  }> = {};
217
222
  const schedule = args.getString("schedule");
218
223
  const promptFile = args.getString("prompt-file");
@@ -227,6 +232,8 @@ export async function jobCommand(): Promise<void> {
227
232
  const statelessRaw = args.getString("stateless");
228
233
  const agent = args.getString("agent");
229
234
  const noAgent = args.getBool("agent");
235
+ const employeeFlag = args.getString("employee");
236
+ const noEmployee = args.getBool("employee");
230
237
 
231
238
  if (schedule) fields.schedule = schedule;
232
239
  if (prompt) fields.prompt = prompt;
@@ -240,6 +247,8 @@ export async function jobCommand(): Promise<void> {
240
247
  if (statelessRaw) fields.stateless = ["yes", "y", "true", "t", "1"].includes(statelessRaw.toLowerCase());
241
248
  if (agent) fields.agent = agent;
242
249
  if (noAgent === false) fields.agent = null;
250
+ if (employeeFlag) fields.employee = employeeFlag;
251
+ if (noEmployee === false) fields.employee = null;
243
252
  const modelFlag = args.getString("model");
244
253
  const noModel = args.getBool("model");
245
254
  if (modelFlag) fields.model = modelFlag;
@@ -247,7 +256,7 @@ export async function jobCommand(): Promise<void> {
247
256
 
248
257
  if (Object.keys(fields).length === 0) {
249
258
  fail(
250
- "Nothing to update. Pass at least one flag (--schedule, --prompt, --type, --always, --stateless, --model, --agent).",
259
+ "Nothing to update. Pass at least one flag (--schedule, --prompt, --type, --always, --stateless, --model, --agent, --employee).",
251
260
  );
252
261
  }
253
262
 
@@ -276,6 +285,7 @@ export async function jobCommand(): Promise<void> {
276
285
  console.log(` enabled: ${job.enabled}`);
277
286
  console.log(` always: ${job.always}`);
278
287
  if (job.agent) console.log(` agent: ${job.agent}`);
288
+ if (job.employee) console.log(` employee: ${job.employee}`);
279
289
  if (job.model) console.log(` model: ${job.model}`);
280
290
  if (job.stateless) console.log(` stateless: true`);
281
291
  console.log(` prompt: ${job.prompt}`);
package/src/cli/status.ts CHANGED
@@ -261,8 +261,9 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
261
261
  const staleText = stale ? " ⚠ stale" : "";
262
262
 
263
263
  const agentTag = job.agent ? ` [${job.agent}]` : "";
264
+ const empTag = job.employee ? ` [emp:${job.employee}]` : "";
264
265
  console.log(
265
- ` ${job.enabled ? "\u25cf" : "\u25cb"} ${job.name.padEnd(20)} ${job.enabled ? "enabled" : "disabled"}${agentTag}`,
266
+ ` ${job.enabled ? "\u25cf" : "\u25cb"} ${job.name.padEnd(20)} ${job.enabled ? "enabled" : "disabled"}${agentTag}${empTag}`,
266
267
  );
267
268
  console.log(
268
269
  ` ${statusIcon} ${status} last: ${lastText} next: ${nextText} duration: ${durationText}${staleText}`,
@@ -49,6 +49,7 @@ export async function createBackup(silent = false): Promise<string> {
49
49
  if (existsSync(join(home, "self"))) includes.push("self");
50
50
  if (existsSync(join(home, "agents"))) includes.push("agents");
51
51
  if (existsSync(join(home, "skills"))) includes.push("skills");
52
+ if (existsSync(join(home, "employees"))) includes.push("employees");
52
53
 
53
54
  // Database dump
54
55
  const config = getConfig();
@@ -65,10 +66,7 @@ export async function createBackup(silent = false): Promise<string> {
65
66
  if (url.port) pgArgs.push("-p", url.port);
66
67
  if (url.username) pgArgs.push("-U", decodeURIComponent(url.username));
67
68
  if (dbName) pgArgs.push("-d", dbName);
68
- const pgEnv: Record<string, string> = { ...process.env } as Record<
69
- string,
70
- string
71
- >;
69
+ const pgEnv: Record<string, string> = { ...process.env } as Record<string, string>;
72
70
  if (url.password) pgEnv.PGPASSWORD = decodeURIComponent(url.password);
73
71
  const sslmode = url.searchParams.get("sslmode");
74
72
  if (sslmode) pgEnv.PGSSLMODE = sslmode;
@@ -87,9 +85,7 @@ export async function createBackup(silent = false): Promise<string> {
87
85
  dbDumped = true;
88
86
  } else if (!silent) {
89
87
  const stderr = await new Response(pg.stderr).text();
90
- console.log(
91
- ` ⚠ db dump skipped: ${stderr.trim() || `exit ${exitCode}`}`,
92
- );
88
+ console.log(` ⚠ db dump skipped: ${stderr.trim() || `exit ${exitCode}`}`);
93
89
  }
94
90
  }
95
91
 
@@ -34,7 +34,8 @@ export function scanAgents(): AgentInfo[] {
34
34
  for (const { dir, source } of getAgentDirs()) {
35
35
  if (!existsSync(dir)) continue;
36
36
 
37
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
37
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
38
+ for (const entry of entries) {
38
39
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
39
40
 
40
41
  const agentFile = join(dir, entry.name, "AGENT.md");
@@ -0,0 +1,116 @@
1
+ import { existsSync, readFileSync, readdirSync } from "fs";
2
+ import { join } from "path";
3
+ import yaml from "js-yaml";
4
+ import { getNiaHome } from "../utils/paths";
5
+ import { log } from "../utils/log";
6
+ import type { EmployeeInfo } from "../types/employee";
7
+
8
+ function getEmployeesDir(): string {
9
+ return join(getNiaHome(), "employees");
10
+ }
11
+
12
+ export function scanEmployees(): EmployeeInfo[] {
13
+ const employees: EmployeeInfo[] = [];
14
+ const dir = getEmployeesDir();
15
+ if (!existsSync(dir)) return employees;
16
+
17
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
18
+
19
+ for (const entry of entries) {
20
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
21
+
22
+ const empFile = join(dir, entry.name, "EMPLOYEE.md");
23
+ if (!existsSync(empFile)) continue;
24
+
25
+ const content = readFileSync(empFile, "utf8");
26
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
27
+ if (!fmMatch) continue;
28
+
29
+ let meta: Record<string, unknown> = {};
30
+ try {
31
+ meta = (yaml.load(fmMatch[1]) as Record<string, unknown>) || {};
32
+ } catch (err) {
33
+ log.warn({ err, employee: entry.name, path: empFile }, "failed to parse employee metadata, skipping");
34
+ continue;
35
+ }
36
+
37
+ const name = (typeof meta.name === "string" ? meta.name : "") || entry.name;
38
+ const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
39
+
40
+ employees.push({
41
+ name,
42
+ dirName: entry.name,
43
+ project: typeof meta.project === "string" ? meta.project : "",
44
+ repo: typeof meta.repo === "string" ? meta.repo : "",
45
+ role: typeof meta.role === "string" ? meta.role : "Employee",
46
+ model: typeof meta.model === "string" ? meta.model : undefined,
47
+ status:
48
+ meta.status === "onboarding" || meta.status === "active" || meta.status === "paused"
49
+ ? meta.status
50
+ : "onboarding",
51
+ maxSubEmployees: typeof meta.maxSubEmployees === "number" ? meta.maxSubEmployees : 3,
52
+ body,
53
+ created: typeof meta.created === "string" ? meta.created : new Date().toISOString().slice(0, 10),
54
+ parent: typeof meta.parent === "string" ? meta.parent : undefined,
55
+ source: "nia",
56
+ });
57
+ }
58
+
59
+ return employees;
60
+ }
61
+
62
+ export function getEmployee(name: string): EmployeeInfo | undefined {
63
+ return scanEmployees().find((e) => e.name.toLowerCase() === name.toLowerCase());
64
+ }
65
+
66
+ export function getEmployeeDir(name: string): string {
67
+ // Look up actual directory — name in frontmatter may differ from dir name
68
+ const emp = scanEmployees().find((e) => e.name.toLowerCase() === name.toLowerCase());
69
+ if (emp) return join(getEmployeesDir(), emp.dirName);
70
+ // Fallback for new employees being created (not yet on disk)
71
+ return join(getEmployeesDir(), name);
72
+ }
73
+
74
+ export function getEmployeesSummary(): string {
75
+ const employees = scanEmployees();
76
+ if (employees.length === 0) return "";
77
+ const lines = employees.map((e) => `- @${e.name}: ${e.role} — ${e.project || "(no project)"} [${e.status}]`);
78
+ return `Available employees:\n${lines.join("\n")}`;
79
+ }
80
+
81
+ export function listEmployeesForMcp(): string {
82
+ const employees = scanEmployees();
83
+ if (employees.length === 0) return "No employees found.";
84
+ return JSON.stringify(
85
+ employees.map((e) => ({
86
+ name: e.name,
87
+ role: e.role,
88
+ project: e.project,
89
+ repo: e.repo,
90
+ status: e.status,
91
+ model: e.model,
92
+ })),
93
+ null,
94
+ 2,
95
+ );
96
+ }
97
+
98
+ /** Injected into employee prompt only when status=onboarding. */
99
+ export const ONBOARDING_INSTRUCTIONS = `## Onboarding
100
+
101
+ You are in onboarding status. Be proactive — don't wait for the user to drive. You're a co-founder getting up to speed, not an assistant being briefed.
102
+
103
+ IMPORTANT: One thing at a time. Each message should focus on ONE step. Don't dump all steps on the user at once. Move to the next step only after the current one is resolved.
104
+
105
+ During the brief, don't just record — challenge. Ask follow-up questions. If the vision sounds vague, say so. If the goals are unrealistic, push back. If something sounds like it matters more than what the user is focused on, flag it.
106
+
107
+ ### Steps (do these in order, one per message)
108
+ 1. **Identity** — If your name is a placeholder (starts with "new-employee"), suggest 3-4 real names and ask the user to pick. Update the name field in your EMPLOYEE.md frontmatter. Do NOT rename the directory — the system resolves it from frontmatter.
109
+ 2. **Project & Repo** — If project or repo are empty, ask what you'll be working on. Get the repo path. Update your EMPLOYEE.md.
110
+ 3. **Brief** — Ask the user about the project: goals, what's working, what's not, their vision. Save to onboarding/brief.md.
111
+ 4. **Self-Discovery** — Explore the repo autonomously. Read code, README, recent commits, deployment config. Save findings to onboarding/discovery.md. Report back to user for corrections.
112
+ 5. **Initial Plan** — Propose top 3-5 priorities with first actions for each. Save to onboarding/plan.md. Get user approval.
113
+
114
+ After all steps are done, update your EMPLOYEE.md status from "onboarding" to "active".
115
+
116
+ Skip any step where the info is already filled in.`;
@@ -8,6 +8,8 @@ import { appendAudit, readState, writeState } from "../utils/logger";
8
8
  import type { AuditEntry, JobState } from "../types";
9
9
  import { getConfig } from "../utils/config";
10
10
  import { buildSystemPrompt } from "../chat/identity";
11
+ import { buildEmployeePrompt } from "../chat/employee-prompt";
12
+ import { getEmployee } from "./employees";
11
13
  import { scanAgents } from "./agents";
12
14
  import { truncate, formatToolUse } from "../utils/format-activity";
13
15
  import { getMcpServers } from "../mcp";
@@ -300,13 +302,23 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
300
302
  writeState(state);
301
303
 
302
304
  try {
303
- const cwd = homedir();
305
+ let cwd = homedir();
304
306
  let output: RunnerOutput;
305
307
 
306
- // Resolve system prompt: use agent body if job references an agent, else default
308
+ // Resolve system prompt: employee > agent > default
307
309
  let systemPrompt: string;
308
310
  let agentModel: string | undefined;
309
- if (job.agent) {
311
+ if (job.employee) {
312
+ const empPrompt = buildEmployeePrompt(job.employee);
313
+ if (empPrompt) {
314
+ systemPrompt = empPrompt;
315
+ } else {
316
+ systemPrompt = buildSystemPrompt("job");
317
+ }
318
+ const emp = getEmployee(job.employee);
319
+ if (emp?.model) agentModel = emp.model;
320
+ if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
321
+ } else if (job.agent) {
310
322
  const agents = scanAgents();
311
323
  const agentDef = agents.find((a) => a.name === job.agent);
312
324
  if (agentDef) {
@@ -26,7 +26,8 @@ export function scanSkills(): SkillInfo[] {
26
26
  for (const { dir, source } of SKILL_DIRS) {
27
27
  if (!existsSync(dir)) continue;
28
28
 
29
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
29
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
30
+ for (const entry of entries) {
30
31
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
31
32
 
32
33
  const skillFile = join(dir, entry.name, "SKILL.md");
@@ -0,0 +1,7 @@
1
+ import type postgres from "postgres";
2
+
3
+ export const name = "015_jobs_employee";
4
+
5
+ export async function up(sql: postgres.Sql): Promise<void> {
6
+ await sql`ALTER TABLE jobs ADD COLUMN IF NOT EXISTS employee TEXT`;
7
+ }
@@ -40,6 +40,7 @@ export interface Job {
40
40
  always: boolean;
41
41
  scheduleType: ScheduleType;
42
42
  agent: string | null;
43
+ employee: string | null;
43
44
  model: string | null;
44
45
  stateless: boolean;
45
46
  nextRunAt: string | null;
@@ -57,6 +58,7 @@ function toJob(r: Record<string, any>): Job {
57
58
  always: r.always ?? false,
58
59
  scheduleType: r.schedule_type || "cron",
59
60
  agent: r.agent || null,
61
+ employee: r.employee || null,
60
62
  model: r.model || null,
61
63
  stateless: r.stateless ?? false,
62
64
  nextRunAt: r.next_run_at ? String(r.next_run_at) : null,
@@ -81,6 +83,7 @@ export async function create(
81
83
  agent?: string,
82
84
  stateless = false,
83
85
  model?: string,
86
+ employee?: string,
84
87
  ): Promise<void> {
85
88
  validateSchedule(schedule, scheduleType);
86
89
  const existing = await get(name);
@@ -89,8 +92,8 @@ export async function create(
89
92
  }
90
93
  const sql = getSql();
91
94
  await sql`
92
- INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent, stateless, model)
93
- VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null}, ${stateless}, ${model ?? null})
95
+ INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent, stateless, model, employee)
96
+ VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null}, ${stateless}, ${model ?? null}, ${employee ?? null})
94
97
  `;
95
98
  await notifyChange();
96
99
  }
@@ -98,14 +101,14 @@ export async function create(
98
101
  export async function list(): Promise<Job[]> {
99
102
  const sql = getSql();
100
103
  const rows =
101
- await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
104
+ await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
102
105
  return rows.map(toJob);
103
106
  }
104
107
 
105
108
  export async function get(name: string): Promise<Job | null> {
106
109
  const sql = getSql();
107
110
  const rows =
108
- await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
111
+ await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
109
112
  return rows.length > 0 ? toJob(rows[0]) : null;
110
113
  }
111
114
 
@@ -117,6 +120,7 @@ export async function update(
117
120
  enabled: boolean;
118
121
  always: boolean;
119
122
  agent: string | null;
123
+ employee: string | null;
120
124
  model: string | null;
121
125
  stateless: boolean;
122
126
  scheduleType: ScheduleType;
@@ -132,6 +136,7 @@ export async function update(
132
136
  const enabled = fields.enabled ?? existing.enabled;
133
137
  const always = fields.always ?? existing.always;
134
138
  const agent = fields.agent !== undefined ? fields.agent : existing.agent;
139
+ const employee = fields.employee !== undefined ? fields.employee : existing.employee;
135
140
  const model = fields.model !== undefined ? fields.model : existing.model;
136
141
  const stateless = fields.stateless ?? existing.stateless;
137
142
 
@@ -144,13 +149,13 @@ export async function update(
144
149
  const nextRun = computeInitialNextRun(scheduleType, schedule, getConfig().timezone);
145
150
  await sql`
146
151
  UPDATE jobs
147
- SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, model = ${model}, stateless = ${stateless}, next_run_at = ${nextRun}, updated_at = NOW()
152
+ SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, employee = ${employee}, model = ${model}, stateless = ${stateless}, next_run_at = ${nextRun}, updated_at = NOW()
148
153
  WHERE name = ${name}
149
154
  `;
150
155
  } else {
151
156
  await sql`
152
157
  UPDATE jobs
153
- SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, model = ${model}, stateless = ${stateless}, updated_at = NOW()
158
+ SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, employee = ${employee}, model = ${model}, stateless = ${stateless}, updated_at = NOW()
154
159
  WHERE name = ${name}
155
160
  `;
156
161
  }
@@ -168,14 +173,14 @@ export async function remove(name: string): Promise<boolean> {
168
173
  export async function listEnabled(): Promise<Job[]> {
169
174
  const sql = getSql();
170
175
  const rows =
171
- await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
176
+ await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
172
177
  return rows.map(toJob);
173
178
  }
174
179
 
175
180
  export async function listDue(): Promise<Job[]> {
176
181
  const sql = getSql();
177
182
  const rows = await sql`
178
- SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at
183
+ SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at
179
184
  FROM jobs
180
185
  WHERE enabled = TRUE AND next_run_at <= NOW()
181
186
  ORDER BY next_run_at
package/src/mcp/server.ts CHANGED
@@ -23,6 +23,10 @@ export function createNiaMcpServer() {
23
23
  .string()
24
24
  .optional()
25
25
  .describe("Agent name to use for this job (loads agent's AGENT.md as system prompt)"),
26
+ employee: z
27
+ .string()
28
+ .optional()
29
+ .describe("Employee name to use for this job (loads employee identity, runs in employee's repo)"),
26
30
  stateless: z
27
31
  .boolean()
28
32
  .default(false)
@@ -38,7 +42,7 @@ export function createNiaMcpServer() {
38
42
  ),
39
43
  tool(
40
44
  "update_job",
41
- "Update an existing job's schedule, prompt, always flag, agent, model, stateless, or schedule_type. Only pass fields you want to change.",
45
+ "Update an existing job's schedule, prompt, always flag, agent, employee, model, stateless, or schedule_type. Only pass fields you want to change.",
42
46
  {
43
47
  name: z.string().describe("Job name to update"),
44
48
  schedule: z
@@ -48,6 +52,7 @@ export function createNiaMcpServer() {
48
52
  prompt: z.string().optional().describe("New prompt"),
49
53
  always: z.boolean().optional().describe("If true, runs 24/7 ignoring active hours"),
50
54
  agent: z.string().nullable().optional().describe("Agent name (set null to remove agent)"),
55
+ employee: z.string().nullable().optional().describe("Employee name (set null to remove employee)"),
51
56
  model: z.string().nullable().optional().describe("Model override (set null to remove and use default)"),
52
57
  stateless: z
53
58
  .boolean()
@@ -306,6 +311,14 @@ export function createNiaMcpServer() {
306
311
  content: [{ type: "text" as const, text: handlers.listAgents() }],
307
312
  }),
308
313
  ),
314
+ tool(
315
+ "list_employees",
316
+ "List all employees with their role, project, status, and model. Employees are persistent co-founders/team members scoped to projects.",
317
+ {},
318
+ async () => ({
319
+ content: [{ type: "text" as const, text: handlers.listEmployees() }],
320
+ }),
321
+ ),
309
322
  ],
310
323
  });
311
324
  }
package/src/mcp/tools.ts CHANGED
@@ -9,6 +9,7 @@ import { getChannel } from "../channels/registry";
9
9
  import { log } from "../utils/log";
10
10
  import { classifyMime } from "../utils/attachment";
11
11
  import { scanAgents } from "../core/agents";
12
+ import { listEmployeesForMcp } from "../core/employees";
12
13
 
13
14
  export async function listJobs(): Promise<string> {
14
15
  const jobs = await Job.list();
@@ -23,6 +24,7 @@ export async function addJob(args: {
23
24
  schedule_type?: ScheduleType;
24
25
  always?: boolean;
25
26
  agent?: string;
27
+ employee?: string;
26
28
  model?: string;
27
29
  stateless?: boolean;
28
30
  }): Promise<string> {
@@ -42,10 +44,12 @@ export async function addJob(args: {
42
44
  args.agent,
43
45
  stateless,
44
46
  args.model,
47
+ args.employee,
45
48
  );
46
49
  const agentNote = args.agent ? ` [agent: ${args.agent}]` : "";
50
+ const employeeNote = args.employee ? ` [employee: ${args.employee}]` : "";
47
51
  const modelNote = args.model ? ` [model: ${args.model}]` : "";
48
- return `Job "${args.name}" created (${scheduleType}: ${args.schedule})${agentNote}${modelNote}. Next run: ${nextRunAt.toISOString()}`;
52
+ return `Job "${args.name}" created (${scheduleType}: ${args.schedule})${agentNote}${employeeNote}${modelNote}. Next run: ${nextRunAt.toISOString()}`;
49
53
  }
50
54
 
51
55
  export async function updateJob(args: {
@@ -54,6 +58,7 @@ export async function updateJob(args: {
54
58
  prompt?: string;
55
59
  always?: boolean;
56
60
  agent?: string | null;
61
+ employee?: string | null;
57
62
  model?: string | null;
58
63
  stateless?: boolean;
59
64
  schedule_type?: "cron" | "interval" | "once";
@@ -65,6 +70,7 @@ export async function updateJob(args: {
65
70
  stateless: boolean;
66
71
  model: string | null;
67
72
  agent: string | null;
73
+ employee: string | null;
68
74
  scheduleType: "cron" | "interval" | "once";
69
75
  }> = {};
70
76
  if (args.schedule) fields.schedule = args.schedule;
@@ -73,10 +79,11 @@ export async function updateJob(args: {
73
79
  if (args.stateless !== undefined) fields.stateless = args.stateless;
74
80
  if (args.model !== undefined) fields.model = args.model;
75
81
  if (args.agent !== undefined) fields.agent = args.agent;
82
+ if (args.employee !== undefined) fields.employee = args.employee;
76
83
  if (args.schedule_type) fields.scheduleType = args.schedule_type;
77
84
 
78
85
  if (Object.keys(fields).length === 0)
79
- return "Nothing to update. Pass at least one field (schedule, prompt, always, stateless, model, agent, or schedule_type).";
86
+ return "Nothing to update. Pass at least one field (schedule, prompt, always, stateless, model, agent, employee, or schedule_type).";
80
87
 
81
88
  const updated = await Job.update(args.name, fields);
82
89
  if (!updated) return `Job "${args.name}" not found.`;
@@ -407,3 +414,7 @@ export function listAgents(): string {
407
414
  2,
408
415
  );
409
416
  }
417
+
418
+ export function listEmployees(): string {
419
+ return listEmployeesForMcp();
420
+ }
@@ -0,0 +1,14 @@
1
+ export interface EmployeeInfo {
2
+ name: string;
3
+ dirName: string;
4
+ project: string;
5
+ repo: string;
6
+ role: string;
7
+ model?: string;
8
+ status: "onboarding" | "active" | "paused";
9
+ maxSubEmployees: number;
10
+ body: string;
11
+ created: string;
12
+ parent?: string;
13
+ source: string;
14
+ }
@@ -17,8 +17,12 @@ export interface SendCallbacks {
17
17
  export interface ChatEngine {
18
18
  sessionId: string | null;
19
19
  room: string;
20
- send(userMessage: string, callbacks?: SendCallbacks, attachments?: import("./attachment").Attachment[]): Promise<SendResult>;
21
- close(): void;
20
+ send(
21
+ userMessage: string,
22
+ callbacks?: SendCallbacks,
23
+ attachments?: import("./attachment").Attachment[],
24
+ ): Promise<SendResult>;
25
+ close(): Promise<void>;
22
26
  }
23
27
 
24
28
  export interface EngineOptions {
@@ -27,4 +31,7 @@ export interface EngineOptions {
27
31
  /** true = resume latest session, or pass a specific session ID */
28
32
  resume: boolean | string;
29
33
  mcpServers?: Record<string, unknown>;
34
+ employee?: string;
35
+ agent?: string;
36
+ job?: string;
30
37
  }
@@ -9,3 +9,4 @@ export type { Config, ChannelsConfig, TelegramConfig, SlackConfig } from "./conf
9
9
  export type { Paths } from "./paths";
10
10
  export type { SaveMessageParams, RoomStats, RecentMessage, SearchResult, SessionMessage } from "./message";
11
11
  export type { AgentInfo } from "./agent";
12
+ export type { EmployeeInfo } from "./employee";
package/src/types/job.ts CHANGED
@@ -5,6 +5,7 @@ export interface JobInput {
5
5
  schedule: string;
6
6
  prompt: string;
7
7
  agent?: string | null;
8
+ employee?: string | null;
8
9
  model?: string | null;
9
10
  stateless?: boolean;
10
11
  }
@@ -11,4 +11,5 @@ export interface Paths {
11
11
  skillsDir: string;
12
12
  imagesDir: string;
13
13
  watchesDir: string;
14
+ employeesDir: string;
14
15
  }
@@ -21,5 +21,6 @@ export function getPaths(): Paths {
21
21
  skillsDir: resolve(home, "skills"),
22
22
  imagesDir: resolve(home, "images"),
23
23
  watchesDir: resolve(home, "watches"),
24
+ employeesDir: resolve(home, "employees"),
24
25
  };
25
26
  }