nolo-cli 0.1.21 → 0.1.22

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.
@@ -47,12 +47,30 @@ export interface SkillEvalConfig {
47
47
  cases: SkillEvalCase[];
48
48
  }
49
49
 
50
+ export interface WorkflowReferenceConfig {
51
+ version: "0.1";
52
+ kind: "workflow";
53
+ id?: string;
54
+ name: string;
55
+ description: string;
56
+ defaultAgent?: string;
57
+ inputs?: string[];
58
+ recommendedTools?: string[];
59
+ requiredTools?: string[];
60
+ requiredOutputs?: string[];
61
+ gates?: string[];
62
+ budgetTier?: SkillBudgetTier;
63
+ contextStrategy?: string;
64
+ failureProtocol?: string;
65
+ }
66
+
50
67
  export interface PageSkillMetadata {
51
68
  kind?: SkillDocKind;
52
69
  requiredSkills?: string[];
53
70
  recommendedSkills?: string[];
54
71
  skillConfig?: SkillDocConfig;
55
72
  evalConfig?: SkillEvalConfig;
73
+ workflowConfig?: WorkflowReferenceConfig;
56
74
  }
57
75
 
58
76
  export interface ParsedSkillDocProtocol {
@@ -71,6 +89,7 @@ export interface ParsedExternalSkillMarkdown {
71
89
 
72
90
  const SKILL_CONFIG_BLOCK = "skill-config";
73
91
  const EVAL_CONFIG_BLOCK = "eval-config";
92
+ const WORKFLOW_CONFIG_BLOCK = "workflow-config";
74
93
 
75
94
  const normalizeStringArray = (value: unknown): string[] | undefined => {
76
95
  if (!Array.isArray(value)) return undefined;
@@ -200,6 +219,49 @@ const normalizeEvalConfig = (value: unknown): SkillEvalConfig | undefined => {
200
219
  : undefined;
201
220
  };
202
221
 
222
+ const normalizeWorkflowConfig = (value: unknown): WorkflowReferenceConfig | undefined => {
223
+ if (!value || typeof value !== "object") return undefined;
224
+ const record = value as Record<string, unknown>;
225
+ const name =
226
+ typeof record.name === "string" && record.name.trim()
227
+ ? record.name.trim()
228
+ : "";
229
+ const description =
230
+ typeof record.description === "string" && record.description.trim()
231
+ ? record.description.trim()
232
+ : "";
233
+ if (!name || !description) return undefined;
234
+ const budgetTier = normalizeSkillEnumValue("budgetTier", record.budgetTier);
235
+ return {
236
+ version: "0.1",
237
+ kind: "workflow",
238
+ id:
239
+ typeof record.id === "string" && record.id.trim()
240
+ ? record.id.trim()
241
+ : undefined,
242
+ name,
243
+ description,
244
+ defaultAgent:
245
+ typeof record.defaultAgent === "string" && record.defaultAgent.trim()
246
+ ? record.defaultAgent.trim()
247
+ : undefined,
248
+ inputs: normalizeStringArray(record.inputs),
249
+ recommendedTools: normalizeStringArray(record.recommendedTools),
250
+ requiredTools: normalizeStringArray(record.requiredTools),
251
+ requiredOutputs: normalizeStringArray(record.requiredOutputs),
252
+ gates: normalizeStringArray(record.gates),
253
+ budgetTier,
254
+ contextStrategy:
255
+ typeof record.contextStrategy === "string" && record.contextStrategy.trim()
256
+ ? record.contextStrategy.trim()
257
+ : undefined,
258
+ failureProtocol:
259
+ typeof record.failureProtocol === "string" && record.failureProtocol.trim()
260
+ ? record.failureProtocol.trim()
261
+ : undefined,
262
+ };
263
+ };
264
+
203
265
  const normalizePageSkillMetadata = (
204
266
  value: unknown,
205
267
  fallbackTools?: string[]
@@ -214,17 +276,22 @@ const normalizePageSkillMetadata = (
214
276
  recommendedSkills: normalizeStringArray(record.recommendedSkills),
215
277
  skillConfig: normalizeSkillConfig(record.skillConfig, fallbackTools),
216
278
  evalConfig: normalizeEvalConfig(record.evalConfig),
279
+ workflowConfig: normalizeWorkflowConfig(record.workflowConfig),
217
280
  };
218
281
 
219
282
  if (meta.skillConfig && !meta.kind) {
220
283
  meta.kind = "skill";
221
284
  }
285
+ if (meta.workflowConfig && !meta.kind) {
286
+ meta.kind = "instruction";
287
+ }
222
288
 
223
289
  return meta.kind ||
224
290
  meta.requiredSkills ||
225
291
  meta.recommendedSkills ||
226
292
  meta.skillConfig ||
227
- meta.evalConfig
293
+ meta.evalConfig ||
294
+ meta.workflowConfig
228
295
  ? meta
229
296
  : undefined;
230
297
  };
@@ -268,9 +335,13 @@ export const parseSkillDocProtocol = (
268
335
  const source = typeof markdown === "string" ? markdown : "";
269
336
  const skillBlock = parseYamlObject(extractCommentBlock(source, SKILL_CONFIG_BLOCK));
270
337
  const evalBlock = parseYamlObject(extractCommentBlock(source, EVAL_CONFIG_BLOCK));
338
+ const workflowBlock = parseYamlObject(extractCommentBlock(source, WORKFLOW_CONFIG_BLOCK));
271
339
  const cleanedContent = removeCommentBlock(
272
- removeCommentBlock(source, SKILL_CONFIG_BLOCK),
273
- EVAL_CONFIG_BLOCK
340
+ removeCommentBlock(
341
+ removeCommentBlock(source, SKILL_CONFIG_BLOCK),
342
+ EVAL_CONFIG_BLOCK
343
+ ),
344
+ WORKFLOW_CONFIG_BLOCK
274
345
  )
275
346
  .replace(/\n{3,}/g, "\n\n")
276
347
  .trim();
@@ -292,6 +363,7 @@ export const parseSkillDocProtocol = (
292
363
  }
293
364
  : {}),
294
365
  ...(evalBlock ? { evalConfig: evalBlock } : {}),
366
+ ...(workflowBlock ? { workflowConfig: workflowBlock } : {}),
295
367
  },
296
368
  fallbackTools
297
369
  );
@@ -364,15 +436,35 @@ export const buildEvalConfigComment = (config: SkillEvalConfig): string =>
364
436
  cases: config.cases,
365
437
  })}\n-->`;
366
438
 
439
+ export const buildWorkflowConfigComment = (config: WorkflowReferenceConfig): string =>
440
+ `<!-- ${WORKFLOW_CONFIG_BLOCK}\n${yamlBlock({
441
+ version: config.version,
442
+ kind: config.kind,
443
+ ...(config.id ? { id: config.id } : {}),
444
+ name: config.name,
445
+ description: config.description,
446
+ ...(config.defaultAgent ? { defaultAgent: config.defaultAgent } : {}),
447
+ ...(config.inputs?.length ? { inputs: config.inputs } : {}),
448
+ ...(config.recommendedTools?.length ? { recommendedTools: config.recommendedTools } : {}),
449
+ ...(config.requiredTools?.length ? { requiredTools: config.requiredTools } : {}),
450
+ ...(config.requiredOutputs?.length ? { requiredOutputs: config.requiredOutputs } : {}),
451
+ ...(config.gates?.length ? { gates: config.gates } : {}),
452
+ ...(config.budgetTier ? { budgetTier: config.budgetTier } : {}),
453
+ ...(config.contextStrategy ? { contextStrategy: config.contextStrategy } : {}),
454
+ ...(config.failureProtocol ? { failureProtocol: config.failureProtocol } : {}),
455
+ })}\n-->`;
456
+
367
457
  export const buildSkillDocMarkdown = (options: {
368
458
  body?: string;
369
459
  skillConfig: SkillDocConfig;
370
460
  evalConfig?: SkillEvalConfig;
461
+ workflowConfig?: WorkflowReferenceConfig;
371
462
  }): string => {
372
463
  const sections = [
373
464
  options.body?.trim() || "",
374
465
  buildSkillConfigComment(options.skillConfig),
375
466
  options.evalConfig ? buildEvalConfigComment(options.evalConfig) : "",
467
+ options.workflowConfig ? buildWorkflowConfigComment(options.workflowConfig) : "",
376
468
  ].filter(Boolean);
377
469
  return sections.join("\n\n").trim();
378
470
  };
@@ -7,6 +7,7 @@ import {
7
7
  runAgentTurn,
8
8
  shouldUseScriptBridge,
9
9
  } from "./agentRun";
10
+ import { NOLO_PROJECT_MANAGER_AGENT_KEY } from "../agentAliases";
10
11
 
11
12
  class CaptureOutput extends Writable {
12
13
  chunks: string[] = [];
@@ -571,7 +572,7 @@ describe("cli agent run client", () => {
571
572
 
572
573
  const result = await runAgentTurn({
573
574
  agentName: "pm",
574
- agentKey: "nolo-project-manager",
575
+ agentKey: "agent-custom-platform-tools",
575
576
  serverUrl: "https://nolo.chat",
576
577
  message: "write task rows",
577
578
  scriptDir: "C:/missing/scripts",
@@ -608,13 +609,60 @@ describe("cli agent run client", () => {
608
609
 
609
610
  expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server" });
610
611
  expect(httpCalls).toHaveLength(1);
611
- expect(httpCalls[0]?.body.agentKey).toBe("nolo-project-manager");
612
+ expect(httpCalls[0]?.body.agentKey).toBe("agent-custom-platform-tools");
612
613
  expect(output.text()).toContain("auto runtime: skipping local runtime");
613
614
  expect(output.text()).toContain("queryTableRows, addTableRow, updateTableRow");
614
615
  expect(output.text()).toContain("pm -> working");
615
616
  expect(output.text()).toContain("pm > server ok");
616
617
  });
617
618
 
619
+ test("auto mode skips known platform agents when local config cannot be read", async () => {
620
+ const output = new CaptureOutput();
621
+ const httpCalls: Array<{ url: string; body: any }> = [];
622
+
623
+ const result = await runAgentTurn({
624
+ agentName: "nolo-project-manager",
625
+ agentKey: NOLO_PROJECT_MANAGER_AGENT_KEY,
626
+ serverUrl: "https://us.nolo.chat",
627
+ message: "write task rows",
628
+ scriptDir: "C:/missing/scripts",
629
+ env: { AUTH_TOKEN: "token-123", OPENAI_API_KEY: "local-provider-present" },
630
+ output,
631
+ runtimeMode: "auto",
632
+ localRuntimeAdapter: {
633
+ host: "cli",
634
+ capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
635
+ loadAgentConfig: async () => {
636
+ throw new Error("Database failed to open: LOCK");
637
+ },
638
+ loadDialogHistory: async () => {
639
+ throw new Error("local runtime should be skipped");
640
+ },
641
+ saveTurn: async () => {
642
+ throw new Error("local runtime should be skipped");
643
+ },
644
+ resolveProvider: async () => {
645
+ throw new Error("local provider should be skipped");
646
+ },
647
+ executeTool: async () => {
648
+ throw new Error("local tools should be skipped");
649
+ },
650
+ },
651
+ scriptPathExists: () => false,
652
+ fetchImpl: async (url, init) => {
653
+ httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
654
+ return Response.json({ content: "server ok", dialogId: "dialog-server" });
655
+ },
656
+ });
657
+
658
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server" });
659
+ expect(httpCalls).toHaveLength(1);
660
+ expect(httpCalls[0]?.body.agentKey).toBe(NOLO_PROJECT_MANAGER_AGENT_KEY);
661
+ expect(output.text()).toContain("known platform agent");
662
+ expect(output.text()).toContain("nolo-project-manager -> working");
663
+ expect(output.text()).toContain("nolo-project-manager > server ok");
664
+ });
665
+
618
666
  test("builds the default local adapter when env requests local mode", async () => {
619
667
  const output = new CaptureOutput();
620
668
  const builtModes: string[] = [];
@@ -1,6 +1,13 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { runLocalAgentTurn } from "../agentRuntimeLocal";
4
+ import {
5
+ FRONTEND_IMPLEMENTER_AGENT_KEY,
6
+ NOLO_FULLSTACK_AGENT_KEY,
7
+ NOLO_PROJECT_MANAGER_AGENT_KEY,
8
+ NOLO_REVIEW_AGENT_KEY,
9
+ NOLO_SENIOR_FULLSTACK_AGENT_KEY,
10
+ } from "../agentAliases";
4
11
  import type { LocalAgentToolEvent } from "../agent-runtime/localLoop";
5
12
  import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
6
13
  import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
@@ -84,6 +91,38 @@ const SERVER_PLATFORM_TOOL_NAMES = new Set([
84
91
  "updateTableRows",
85
92
  ]);
86
93
 
94
+ const KNOWN_SERVER_PLATFORM_AGENT_KEYS = new Set([
95
+ FRONTEND_IMPLEMENTER_AGENT_KEY,
96
+ NOLO_FULLSTACK_AGENT_KEY,
97
+ NOLO_PROJECT_MANAGER_AGENT_KEY,
98
+ NOLO_REVIEW_AGENT_KEY,
99
+ NOLO_SENIOR_FULLSTACK_AGENT_KEY,
100
+ ]);
101
+
102
+ const KNOWN_SERVER_PLATFORM_AGENT_ALIASES = new Set([
103
+ "code-review",
104
+ "frontend",
105
+ "frontend-agent",
106
+ "frontend-implementer",
107
+ "full-stack",
108
+ "fullstack",
109
+ "nolo code review",
110
+ "nolo frontend",
111
+ "nolo fullstack",
112
+ "nolo project manager",
113
+ "nolo reviewer",
114
+ "nolo-code-review",
115
+ "nolo-frontend",
116
+ "nolo-fullstack",
117
+ "nolo-pm",
118
+ "nolo-project-manager",
119
+ "nolo-reviewer",
120
+ "pm",
121
+ "project-manager",
122
+ "review",
123
+ "reviewer",
124
+ ]);
125
+
87
126
  export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
88
127
  return !decision.hasAuthToken && decision.scriptPathExists;
89
128
  }
@@ -92,6 +131,16 @@ export function findServerPlatformTools(toolNames?: string[]) {
92
131
  if (!Array.isArray(toolNames)) return [];
93
132
  return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
94
133
  }
134
+
135
+ function normalizeAgentRef(ref?: string) {
136
+ return ref?.trim().toLowerCase().replace(/\s+/g, " ");
137
+ }
138
+
139
+ function isKnownServerPlatformAgent(options: RunAgentTurnOptions) {
140
+ if (KNOWN_SERVER_PLATFORM_AGENT_KEYS.has(options.agentKey)) return true;
141
+ const normalizedKey = normalizeAgentRef(options.agentKey);
142
+ return Boolean(normalizedKey && KNOWN_SERVER_PLATFORM_AGENT_ALIASES.has(normalizedKey));
143
+ }
95
144
 
96
145
  function resolveAuthToken(env: EnvLike) {
97
146
  return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
@@ -126,6 +175,13 @@ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
126
175
  }
127
176
 
128
177
  async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
178
+ if (isKnownServerPlatformAgent(options)) {
179
+ options.output.write(
180
+ `[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a known platform agent. ` +
181
+ "Use --local explicitly to force local workspace tools.\n"
182
+ );
183
+ return true;
184
+ }
129
185
  const adapter = resolveLocalRuntimeAdapter(options);
130
186
  if (!adapter) return false;
131
187
  let agentConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {