niahere 0.2.63 → 0.2.65

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/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");
@@ -58,8 +58,9 @@ Nia uses a two-stage memory architecture. You are stage 1.
58
58
 
59
59
  Your persona includes guidance to "save proactively" — that guidance applies
60
60
  to LIVE chat, where you act on immediate user instruction. In THIS
61
- consolidation pass, your default action is to do nothing. You only act when
62
- you can point to a specific user turn that taught you something durable.
61
+ consolidation pass, be selective but not paralyzed. If you see a genuine
62
+ learning, stage it. The promoter handles quality gating your job is to
63
+ not miss real signals, not to be maximally conservative.
63
64
 
64
65
  ## Transcript
65
66
 
@@ -81,13 +82,20 @@ Answer these questions silently. If the answer to all of them is "nothing",
81
82
  stop here and do not write anything.
82
83
 
83
84
  1. What did the user correct, clarify, or teach you in this session?
85
+ (Includes: "no, do it this way", "don't use X", "always check Y first")
84
86
  2. What NEW fact about the user, their projects, or their systems do you
85
87
  now know that you did not at session start?
88
+ (Includes: architecture decisions, workflow patterns, tool preferences,
89
+ team structure, external system details discovered during task execution)
86
90
  3. What decision was made that will constrain future work?
91
+ (Includes: "we're using X not Y", config changes, deployment patterns)
92
+ 4. What did the user explicitly ask to be remembered?
87
93
 
88
- Trivial small talk, greetings, task-execution chatter, and status updates
89
- are NOT answers. If you cannot quote a specific user turn that produced the
90
- learning, you are fishing stop.
94
+ Trivial small talk, greetings, and pure status updates are NOT answers.
95
+ But corrections made DURING task execution ("no, check DynamoDB not S3"),
96
+ architecture learned while debugging ("ah, this service talks to X via Y"),
97
+ and workflow patterns revealed by how the user works — these ARE answers.
98
+ The bar is: would a fresh Nia session benefit from knowing this?
91
99
 
92
100
  ## Step 3 — Update staging.md
93
101
 
@@ -118,7 +126,8 @@ For each substantive answer:
118
126
  - Do NOT write to \`memory.md\` or \`rules.md\`. Only the promoter job can.
119
127
  - Do NOT use \`add_memory\` or \`add_rule\` MCP tools. Edit staging.md directly.
120
128
  - Do NOT message the user.
121
- - Default action is to do nothing. Most sessions have nothing to stage.
129
+ - If nothing qualifies, do nothing. But don't be so conservative that the
130
+ pipeline starves — if you're skipping every session, your bar is too high.
122
131
 
123
132
  Report a one-line summary of what you did: "staged N new / reinforced M /
124
133
  skipped (trivial session)". No preamble.`;
@@ -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
  }