@united-workforce/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/__tests__/adapter-json-roundtrip.test.js +4 -1
  2. package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
  3. package/dist/__tests__/current-role.test.js +15 -3
  4. package/dist/__tests__/current-role.test.js.map +1 -1
  5. package/dist/__tests__/moderator-evaluate.test.js +20 -4
  6. package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
  7. package/dist/__tests__/prompt.test.js +20 -42
  8. package/dist/__tests__/prompt.test.js.map +1 -1
  9. package/dist/__tests__/step-timing.test.js +8 -2
  10. package/dist/__tests__/step-timing.test.js.map +1 -1
  11. package/dist/__tests__/thread-location.test.js +15 -3
  12. package/dist/__tests__/thread-location.test.js.map +1 -1
  13. package/dist/__tests__/thread-resume.test.js +21 -6
  14. package/dist/__tests__/thread-resume.test.js.map +1 -1
  15. package/dist/__tests__/thread-show-status.test.js +10 -2
  16. package/dist/__tests__/thread-show-status.test.js.map +1 -1
  17. package/dist/__tests__/thread-start-cwd-cli.test.js +5 -1
  18. package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
  19. package/dist/__tests__/thread-suspend-step.test.js +4 -1
  20. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  21. package/dist/__tests__/thread-suspended-display.test.js +12 -3
  22. package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
  23. package/dist/__tests__/validate-semantic.test.js +29 -12
  24. package/dist/__tests__/validate-semantic.test.js.map +1 -1
  25. package/dist/__tests__/workflow-resolution.test.js +4 -1
  26. package/dist/__tests__/workflow-resolution.test.js.map +1 -1
  27. package/dist/cli.js +5 -17
  28. package/dist/cli.js.map +1 -1
  29. package/dist/commands/prompt.d.ts +3 -4
  30. package/dist/commands/prompt.d.ts.map +1 -1
  31. package/dist/commands/prompt.js +22 -39
  32. package/dist/commands/prompt.js.map +1 -1
  33. package/dist/commands/thread.d.ts.map +1 -1
  34. package/dist/commands/thread.js +9 -7
  35. package/dist/commands/thread.js.map +1 -1
  36. package/dist/moderator/__tests__/evaluate.test.js +12 -12
  37. package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
  38. package/dist/moderator/evaluate.d.ts.map +1 -1
  39. package/dist/moderator/evaluate.js +1 -7
  40. package/dist/moderator/evaluate.js.map +1 -1
  41. package/dist/validate-semantic.d.ts.map +1 -1
  42. package/dist/validate-semantic.js +4 -12
  43. package/dist/validate-semantic.js.map +1 -1
  44. package/dist/validate.js +3 -3
  45. package/dist/validate.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/__tests__/adapter-json-roundtrip.test.ts +4 -1
  48. package/src/__tests__/current-role.test.ts +15 -3
  49. package/src/__tests__/fixtures/e2e-count.workflow.yaml +2 -1
  50. package/src/__tests__/fixtures/e2e-linear.workflow.yaml +2 -1
  51. package/src/__tests__/fixtures/e2e-loop.workflow.yaml +2 -1
  52. package/src/__tests__/fixtures/e2e-mustache.workflow.yaml +2 -1
  53. package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +2 -1
  54. package/src/__tests__/moderator-evaluate.test.ts +21 -4
  55. package/src/__tests__/prompt.test.ts +19 -46
  56. package/src/__tests__/step-timing.test.ts +8 -2
  57. package/src/__tests__/thread-location.test.ts +15 -3
  58. package/src/__tests__/thread-resume.test.ts +21 -6
  59. package/src/__tests__/thread-show-status.test.ts +10 -2
  60. package/src/__tests__/thread-start-cwd-cli.test.ts +5 -1
  61. package/src/__tests__/thread-suspend-step.test.ts +4 -1
  62. package/src/__tests__/thread-suspended-display.test.ts +12 -3
  63. package/src/__tests__/validate-semantic.test.ts +36 -16
  64. package/src/__tests__/workflow-resolution.test.ts +4 -1
  65. package/src/cli.ts +4 -20
  66. package/src/commands/prompt.ts +22 -41
  67. package/src/commands/thread.ts +9 -8
  68. package/src/moderator/__tests__/evaluate.test.ts +12 -12
  69. package/src/moderator/evaluate.ts +1 -6
  70. package/src/validate-semantic.ts +4 -13
  71. package/src/validate.ts +3 -3
@@ -34,10 +34,14 @@ roles:
34
34
  $status: { type: string, enum: ["ready"] }
35
35
  graph:
36
36
  $START:
37
- _:
37
+ new:
38
38
  role: planner
39
39
  prompt: "Plan the work"
40
40
  location: null
41
+ resume:
42
+ role: planner
43
+ prompt: "Resume the work"
44
+ location: null
41
45
  planner:
42
46
  ready:
43
47
  role: $END
@@ -66,10 +70,14 @@ roles:
66
70
  question: { type: string }
67
71
  graph:
68
72
  $START:
69
- _:
73
+ new:
70
74
  role: worker
71
75
  prompt: "Start work"
72
76
  location: null
77
+ resume:
78
+ role: worker
79
+ prompt: "Resume work"
80
+ location: null
73
81
  worker:
74
82
  needs_input:
75
83
  role: $SUSPEND
@@ -57,10 +57,14 @@ roles:
57
57
  $status: { type: string, enum: ["ready"] }
58
58
  graph:
59
59
  $START:
60
- _:
60
+ new:
61
61
  role: planner
62
62
  prompt: "Plan the work"
63
63
  location: null
64
+ resume:
65
+ role: planner
66
+ prompt: "Resume the work"
67
+ location: null
64
68
  planner:
65
69
  ready:
66
70
  role: $END
@@ -58,7 +58,10 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
58
58
  },
59
59
  },
60
60
  graph: {
61
- $START: { _: { role: "worker", prompt: "Start work", location: null } },
61
+ $START: {
62
+ new: { role: "worker", prompt: "Start work", location: null },
63
+ resume: { role: "worker", prompt: "Resume work", location: null },
64
+ },
62
65
  worker: {
63
66
  needs_input: {
64
67
  role: "$SUSPEND",
@@ -55,7 +55,10 @@ describe("suspended thread display", () => {
55
55
  },
56
56
  },
57
57
  graph: {
58
- $START: { _: { role: "worker", prompt: "Start work", location: null } },
58
+ $START: {
59
+ new: { role: "worker", prompt: "Start work", location: null },
60
+ resume: { role: "worker", prompt: "Resume work", location: null },
61
+ },
59
62
  worker: {
60
63
  needs_input: {
61
64
  role: "$SUSPEND",
@@ -162,7 +165,10 @@ describe("suspended thread display", () => {
162
165
  },
163
166
  },
164
167
  graph: {
165
- $START: { _: { role: "worker", prompt: "Start work", location: null } },
168
+ $START: {
169
+ new: { role: "worker", prompt: "Start work", location: null },
170
+ resume: { role: "worker", prompt: "Resume work", location: null },
171
+ },
166
172
  worker: {
167
173
  needs_input: {
168
174
  role: "$SUSPEND",
@@ -248,7 +254,10 @@ describe("suspended thread display", () => {
248
254
  },
249
255
  },
250
256
  graph: {
251
- $START: { _: { role: "worker", prompt: "Start work", location: null } },
257
+ $START: {
258
+ new: { role: "worker", prompt: "Start work", location: null },
259
+ resume: { role: "worker", prompt: "Resume work", location: null },
260
+ },
252
261
  },
253
262
  });
254
263
 
@@ -51,7 +51,10 @@ function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
51
51
  },
52
52
  },
53
53
  graph: {
54
- $START: { _: { role: "writer", prompt: "Begin writing", location: null } },
54
+ $START: {
55
+ new: { role: "writer", prompt: "Begin writing", location: null },
56
+ resume: { role: "writer", prompt: "Review previous output and continue", location: null },
57
+ },
55
58
  writer: { done: { role: "reviewer", prompt: "Review this: {{{plan}}}", location: null } },
56
59
  reviewer: {
57
60
  approved: { role: "$END", prompt: "Done: {{{summary}}}", location: null },
@@ -135,27 +138,38 @@ describe("Suite 2: Graph Structure", () => {
135
138
  expect(errors.some((e) => e.includes("$START must be defined in graph"))).toBe(true);
136
139
  });
137
140
 
138
- test("2.2 $START has multiple status keys", () => {
141
+ test("2.2 $START missing resume edge", () => {
139
142
  const wf = makeWorkflow();
140
143
  wf.graph.$START = {
141
- _: { role: "writer", prompt: "Begin", location: null },
142
- other: { role: "reviewer", prompt: "Also", location: null },
144
+ new: { role: "writer", prompt: "Begin", location: null },
143
145
  };
144
146
  const errors = validateWorkflow(wf);
145
147
  expect(
146
- errors.some((e) => e.includes('$START must have exactly one edge with status "_"')),
148
+ errors.some((e) => e.includes('$START must have edges with statuses "new" and "resume"')),
147
149
  ).toBe(true);
148
150
  });
149
151
 
150
- test("2.3 $START edge uses non-_ status", () => {
152
+ test("2.3 $START missing new edge", () => {
151
153
  const wf = makeWorkflow();
152
- wf.graph.$START = { ready: { role: "writer", prompt: "Begin", location: null } };
154
+ wf.graph.$START = {
155
+ resume: { role: "writer", prompt: "Resume", location: null },
156
+ };
153
157
  const errors = validateWorkflow(wf);
154
158
  expect(
155
- errors.some((e) => e.includes('$START must have exactly one edge with status "_"')),
159
+ errors.some((e) => e.includes('$START must have edges with statuses "new" and "resume"')),
156
160
  ).toBe(true);
157
161
  });
158
162
 
163
+ test("2.3b $START with new and resume passes", () => {
164
+ const wf = makeWorkflow();
165
+ wf.graph.$START = {
166
+ new: { role: "writer", prompt: "Begin", location: null },
167
+ resume: { role: "writer", prompt: "Resume", location: null },
168
+ };
169
+ const errors = validateWorkflow(wf);
170
+ expect(errors.some((e) => e.includes("$START must have edges"))).toBe(false);
171
+ });
172
+
159
173
  test("2.4 $END has outgoing edges", () => {
160
174
  const wf = makeWorkflow();
161
175
  wf.graph.$END = { _: { role: "writer", prompt: "Loop", location: null } };
@@ -193,15 +207,18 @@ describe("Suite 2: Graph Structure", () => {
193
207
  });
194
208
 
195
209
  describe("Suite 3: Status-Edge Consistency", () => {
196
- test("3.1 user role using _ graph key is rejected", () => {
210
+ test("3.1 user role using _ graph key is treated as an unknown status", () => {
211
+ // "_" is no longer special-cased — it's just a status key that does not
212
+ // match the role's $status enum, so it surfaces as extra/missing keys.
197
213
  const wf = makeWorkflow();
198
214
  wf.graph.writer = { _: { role: "reviewer", prompt: "Review", location: null } };
199
215
  const errors = validateWorkflow(wf);
200
- expect(
201
- errors.some((e) =>
202
- e.includes('role "writer" must use explicit $status keys in graph, not "_"'),
203
- ),
204
- ).toBe(true);
216
+ expect(errors.some((e) => e.includes('role "writer" graph has extra status keys: _'))).toBe(
217
+ true,
218
+ );
219
+ expect(errors.some((e) => e.includes('role "writer" graph is missing status keys: done'))).toBe(
220
+ true,
221
+ );
205
222
  });
206
223
 
207
224
  test("3.2 user role graph key not matching $status enum", () => {
@@ -240,13 +257,16 @@ describe("Suite 3: Status-Edge Consistency", () => {
240
257
  ).toBe(true);
241
258
  });
242
259
 
243
- test("3.5 multi-exit role with _ key", () => {
260
+ test("3.5 multi-exit role with _ key is treated as an unknown status", () => {
244
261
  const wf = makeWorkflow();
245
262
  wf.graph.reviewer = { _: { role: "$END", prompt: "Done", location: null } };
246
263
  const errors = validateWorkflow(wf);
264
+ expect(errors.some((e) => e.includes('role "reviewer" graph has extra status keys: _'))).toBe(
265
+ true,
266
+ );
247
267
  expect(
248
268
  errors.some((e) =>
249
- e.includes('role "reviewer" must use explicit $status keys in graph, not "_"'),
269
+ e.includes('role "reviewer" graph is missing status keys: approved, rejected'),
250
270
  ),
251
271
  ).toBe(true);
252
272
  });
@@ -38,7 +38,10 @@ function makeMinimalPayload(name: string, description: string): WorkflowPayload
38
38
  },
39
39
  },
40
40
  graph: {
41
- $START: { _: { role: "worker", prompt: "start working", location: null } },
41
+ $START: {
42
+ new: { role: "worker", prompt: "start working", location: null },
43
+ resume: { role: "worker", prompt: "resume working", location: null },
44
+ },
42
45
  worker: { done: { role: "$END", prompt: "done", location: null } },
43
46
  },
44
47
  };
package/src/cli.ts CHANGED
@@ -8,9 +8,7 @@ import {
8
8
  cmdPromptAdapterDeveloping,
9
9
  cmdPromptBootstrap,
10
10
  cmdPromptList,
11
- cmdPromptSetup,
12
11
  cmdPromptUsage,
13
- cmdPromptUsageReference,
14
12
  cmdPromptWorkflowAuthoring,
15
13
  } from "./commands/prompt.js";
16
14
  import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
@@ -509,23 +507,16 @@ prompt.addHelpCommand(false);
509
507
 
510
508
  prompt
511
509
  .command("usage")
512
- .description("Print the complete skill content (all references combined)")
510
+ .description("Print the usage reference (CLI guide + typical workflows)")
513
511
  .action(() => {
514
512
  console.log(cmdPromptUsage());
515
513
  });
516
514
 
517
515
  prompt
518
- .command("setup")
519
- .description("Print setup instructions for installing the uwf skill")
520
- .action(() => {
521
- console.log(cmdPromptSetup());
522
- });
523
-
524
- prompt
525
- .command("usage-reference")
526
- .description("Print the usage reference (CLI guide + typical workflows)")
516
+ .command("bootstrap")
517
+ .description("Print setup instructions for installing uwf skills")
527
518
  .action(() => {
528
- console.log(cmdPromptUsageReference());
519
+ console.log(cmdPromptBootstrap());
529
520
  });
530
521
 
531
522
  prompt
@@ -542,13 +533,6 @@ prompt
542
533
  console.log(cmdPromptAdapterDeveloping());
543
534
  });
544
535
 
545
- prompt
546
- .command("bootstrap")
547
- .description("Print the bootstrap skill YAML for Hermes agents")
548
- .action(() => {
549
- console.log(cmdPromptBootstrap());
550
- });
551
-
552
536
  prompt
553
537
  .command("list")
554
538
  .description("List all available prompt names")
@@ -1,14 +1,13 @@
1
1
  import {
2
2
  generateAdapterDevelopingReference,
3
- generateBootstrapReference,
4
3
  generateUsageReference,
5
4
  generateWorkflowAuthoringReference,
5
+ VERSION,
6
6
  } from "@united-workforce/util";
7
7
 
8
8
  export {
9
9
  generateAdapterDevelopingReference as cmdPromptAdapterDeveloping,
10
- generateBootstrapReference as cmdPromptBootstrap,
11
- generateUsageReference as cmdPromptUsageReference,
10
+ generateUsageReference as cmdPromptUsage,
12
11
  generateWorkflowAuthoringReference as cmdPromptWorkflowAuthoring,
13
12
  };
14
13
 
@@ -16,24 +15,16 @@ const PROMPT_ENTRIES: ReadonlyArray<{ name: string; generate: () => string }> =
16
15
  { name: "usage", generate: generateUsageReference },
17
16
  { name: "workflow-authoring", generate: generateWorkflowAuthoringReference },
18
17
  { name: "adapter-developing", generate: generateAdapterDevelopingReference },
19
- { name: "bootstrap", generate: generateBootstrapReference },
20
18
  ];
21
19
 
22
20
  export function cmdPromptList(): ReadonlyArray<string> {
23
21
  return PROMPT_ENTRIES.map((e) => e.name);
24
22
  }
25
23
 
26
- export function cmdPromptUsage(): string {
27
- return PROMPT_ENTRIES.filter((e) => e.name !== "bootstrap")
28
- .map((e) => e.generate())
29
- .join("\n\n---\n\n");
30
- }
31
-
32
- export function cmdPromptSetup(): string {
33
- return `# uwf Skill Setup
24
+ export function cmdPromptBootstrap(): string {
25
+ return `# uwf Skill Bootstrap
34
26
 
35
- You are being asked to install or update the uwf (United Workforce) skill
36
- so that you know how to use the \`uwf\` CLI for workflow orchestration.
27
+ Install or update the uwf (United Workforce) skills for your agent.
37
28
 
38
29
  ## Steps
39
30
 
@@ -42,40 +33,30 @@ so that you know how to use the \`uwf\` CLI for workflow orchestration.
42
33
  uwf --help
43
34
  \`\`\`
44
35
  If not installed: \`npm install -g @united-workforce/cli\`
36
+ Current version: ${VERSION}
45
37
 
46
- 2. **Clean up old versions of the skill** (if any exist):
47
- - Look for any existing uwf/workflow skill files in your skill directories
48
- (e.g. \`~/.hermes/skills/uwf/SKILL.md\` or similar paths).
49
- - If a skill exists, read its \`version\` from the YAML frontmatter.
50
- - If the version matches the current CLI version (\`uwf --version\`), you're done — no update needed.
51
- - If the version is older, **delete the old skill file** before proceeding.
38
+ 2. **Install the skills** by running each command below and creating a skill from its output:
39
+ - Run \`uwf prompt usage\` create a skill named \`uwf-usage\`
40
+ - Run \`uwf prompt workflow-authoring\` create a skill named \`uwf-workflow-authoring\`
41
+ - Run \`uwf prompt adapter-developing\` create a skill named \`uwf-adapter-developing\`
52
42
 
53
- 3. **Install the bootstrap skill:**
54
- \`\`\`bash
55
- uwf prompt bootstrap > ~/.hermes/skills/uwf/SKILL.md
56
- \`\`\`
57
- The bootstrap prompt already includes complete YAML frontmatter — no editing needed.
43
+ Each command outputs a complete SKILL.md with YAML frontmatter — use your agent framework's skill creation API to save them.
58
44
 
59
- 4. **Verify** the skill is loadable by your agent framework.
45
+ 3. **Verify** the skills are loadable by your agent framework.
60
46
 
61
- ## Individual prompts
47
+ ## Updating
62
48
 
63
- Each prompt outputs a complete SKILL.md with frontmatter pipe directly to a file:
49
+ When \`uwf\` is upgraded, re-run \`uwf prompt bootstrap\` and follow the steps again.
50
+ The skill content is bundled with the CLI — always use \`uwf prompt <name>\` to get
51
+ content matching your installed version.
52
+
53
+ ## Available prompts
64
54
 
65
55
  \`\`\`bash
66
- uwf prompt list # list available prompt names
67
- uwf prompt usage > ~/.hermes/skills/uwf-usage/SKILL.md # CLI usage guide
68
- uwf prompt workflow-authoring > ~/.hermes/skills/uwf-workflow-authoring/SKILL.md
69
- uwf prompt adapter-developing > ~/.hermes/skills/uwf-adapter-developing/SKILL.md
70
- uwf prompt bootstrap > ~/.hermes/skills/uwf/SKILL.md # bootstrap skill
56
+ uwf prompt list # list available prompt names
57
+ uwf prompt usage # CLI usage guide
58
+ uwf prompt workflow-authoring # workflow YAML design guide
59
+ uwf prompt adapter-developing # building agent adapters
71
60
  \`\`\`
72
-
73
- ## Notes
74
-
75
- - The skill content is bundled with the CLI and versioned with it — always use
76
- \`uwf prompt usage\` to get the content matching your installed version.
77
- - Do NOT hand-edit the skill body. If the CLI is updated, re-run \`uwf prompt setup\`
78
- and follow the steps again.
79
- - When upgrading, always delete the old skill first to avoid stale instructions.
80
61
  `;
81
62
  }
@@ -911,7 +911,7 @@ function resolveEvaluateArgs(
911
911
  chain: ChainState,
912
912
  ): { lastRole: string; lastOutput: EvaluateLastOutput } {
913
913
  if (chain.headIsStart) {
914
- return { lastRole: START_ROLE, lastOutput: { [STATUS_KEY]: "_" } };
914
+ return { lastRole: START_ROLE, lastOutput: { [STATUS_KEY]: "new" } };
915
915
  }
916
916
 
917
917
  const lastStep = chain.stepsNewestFirst[0];
@@ -961,6 +961,12 @@ function resolveAgentConfig(
961
961
  agentOverride: string | null,
962
962
  ): AgentConfig {
963
963
  if (agentOverride !== null) {
964
+ // Try config alias first (e.g. "hermes" → config.agents.hermes),
965
+ // then fall back to raw command name (e.g. "uwf-hermes" or "/usr/bin/agent").
966
+ const fromAlias = config.agents[agentOverride as AgentAlias];
967
+ if (fromAlias !== undefined) {
968
+ return fromAlias;
969
+ }
964
970
  return parseAgentOverride(agentOverride);
965
971
  }
966
972
 
@@ -1031,7 +1037,6 @@ function archiveThread(uwf: UwfStore, threadId: ThreadId, _workflow: CasRef, _he
1031
1037
  completeThread(uwf.varStore, threadId, "completed");
1032
1038
  }
1033
1039
 
1034
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: orchestration function with inherent branching
1035
1040
  export async function cmdThreadResume(
1036
1041
  storageRoot: string,
1037
1042
  threadId: ThreadId,
@@ -1095,7 +1100,7 @@ export async function cmdThreadResume(
1095
1100
 
1096
1101
  // status === "completed"
1097
1102
  const workflow = loadWorkflowPayload(uwf, workflowHash);
1098
- const startResult = evaluate(workflow.graph, START_ROLE, {});
1103
+ const startResult = evaluate(workflow.graph, START_ROLE, { [STATUS_KEY]: "resume" });
1099
1104
  if (!startResult.ok) {
1100
1105
  fail(`failed to evaluate $START: ${startResult.error.message}`);
1101
1106
  }
@@ -1107,11 +1112,7 @@ export async function cmdThreadResume(
1107
1112
  }
1108
1113
 
1109
1114
  const startRole = startResult.value.role;
1110
- const completedPromptPrefix = "Previous run completed. Resuming with additional context.";
1111
- const completedResumePrompt =
1112
- supplement !== null && supplement !== ""
1113
- ? `${completedPromptPrefix}\n\n${supplement}`
1114
- : completedPromptPrefix;
1115
+ const completedResumePrompt = buildResumePrompt(startResult.value.prompt, supplement);
1115
1116
 
1116
1117
  const updatedEntry = { ...entry, status: "idle" as const, completedAt: null };
1117
1118
  setThread(uwf.varStore, threadId, updatedEntry);
@@ -6,11 +6,11 @@ describe("Edge prompt template variable resolution", () => {
6
6
  test("returns error when rendered prompt is empty string", () => {
7
7
  const graph = {
8
8
  $START: {
9
- _: { role: "classifier", prompt: "{{{userPrompt}}}", location: null },
9
+ new: { role: "classifier", prompt: "{{{userPrompt}}}", location: null },
10
10
  },
11
11
  };
12
12
 
13
- const result = evaluate(graph, "$START", {});
13
+ const result = evaluate(graph, "$START", { $status: "new" });
14
14
 
15
15
  expect(result.ok).toBe(false);
16
16
  if (!result.ok) {
@@ -22,11 +22,11 @@ describe("Edge prompt template variable resolution", () => {
22
22
  test("returns error when rendered prompt is whitespace-only", () => {
23
23
  const graph = {
24
24
  $START: {
25
- _: { role: "classifier", prompt: " {{{userPrompt}}} ", location: null },
25
+ new: { role: "classifier", prompt: " {{{userPrompt}}} ", location: null },
26
26
  },
27
27
  };
28
28
 
29
- const result = evaluate(graph, "$START", {});
29
+ const result = evaluate(graph, "$START", { $status: "new" });
30
30
 
31
31
  expect(result.ok).toBe(false);
32
32
  if (!result.ok) {
@@ -38,11 +38,11 @@ describe("Edge prompt template variable resolution", () => {
38
38
  test("succeeds when all template variables resolve to non-empty values", () => {
39
39
  const graph = {
40
40
  $START: {
41
- _: { role: "classifier", prompt: "{{{userPrompt}}}", location: null },
41
+ new: { role: "classifier", prompt: "{{{userPrompt}}}", location: null },
42
42
  },
43
43
  };
44
44
 
45
- const result = evaluate(graph, "$START", { userPrompt: "Fix the bug" });
45
+ const result = evaluate(graph, "$START", { $status: "new", userPrompt: "Fix the bug" });
46
46
 
47
47
  expect(result.ok).toBe(true);
48
48
  if (result.ok) {
@@ -53,11 +53,11 @@ describe("Edge prompt template variable resolution", () => {
53
53
  test("succeeds with static (no-variable) prompt", () => {
54
54
  const graph = {
55
55
  $START: {
56
- _: { role: "classifier", prompt: "Classify this input", location: null },
56
+ new: { role: "classifier", prompt: "Classify this input", location: null },
57
57
  },
58
58
  };
59
59
 
60
- const result = evaluate(graph, "$START", {});
60
+ const result = evaluate(graph, "$START", { $status: "new" });
61
61
 
62
62
  expect(result.ok).toBe(true);
63
63
  if (result.ok) {
@@ -68,11 +68,11 @@ describe("Edge prompt template variable resolution", () => {
68
68
  test("succeeds when prompt has mix of static text and unresolved variables", () => {
69
69
  const graph = {
70
70
  $START: {
71
- _: { role: "classifier", prompt: "Please handle: {{{userPrompt}}}", location: null },
71
+ new: { role: "classifier", prompt: "Please handle: {{{userPrompt}}}", location: null },
72
72
  },
73
73
  };
74
74
 
75
- const result = evaluate(graph, "$START", {});
75
+ const result = evaluate(graph, "$START", { $status: "new" });
76
76
 
77
77
  expect(result.ok).toBe(true);
78
78
  if (result.ok) {
@@ -83,11 +83,11 @@ describe("Edge prompt template variable resolution", () => {
83
83
  test("returns error when ALL variables missing and no static text remains", () => {
84
84
  const graph = {
85
85
  $START: {
86
- _: { role: "classifier", prompt: "{{{a}}}{{{b}}}", location: null },
86
+ new: { role: "classifier", prompt: "{{{a}}}{{{b}}}", location: null },
87
87
  },
88
88
  };
89
89
 
90
- const result = evaluate(graph, "$START", {});
90
+ const result = evaluate(graph, "$START", { $status: "new" });
91
91
 
92
92
  expect(result.ok).toBe(false);
93
93
  });
@@ -6,10 +6,7 @@ import type { EvaluateResult, Result } from "./types.js";
6
6
  // Disable HTML escaping — prompts are plain text, not HTML.
7
7
  mustache.escape = (text: string) => text;
8
8
 
9
- const START_ROLE = "$START";
10
9
  const SUSPEND_ROLE = "$SUSPEND";
11
- // $START is a special entry node with no agent output — it always uses this key.
12
- const START_STATUS = "_";
13
10
 
14
11
  type LastOutput = Record<string, unknown>;
15
12
 
@@ -21,9 +18,7 @@ export function evaluate(
21
18
  lastOutput: LastOutput,
22
19
  ): Result<EvaluateResult, Error> {
23
20
  let status: string;
24
- if (lastRole === START_ROLE) {
25
- status = START_STATUS;
26
- } else if (typeof lastOutput[STATUS_KEY] === "string") {
21
+ if (typeof lastOutput[STATUS_KEY] === "string") {
27
22
  status = lastOutput[STATUS_KEY] as string;
28
23
  } else {
29
24
  return {
@@ -97,9 +97,9 @@ function checkGraphStructure(payload: WorkflowPayload, errors: string[]): void {
97
97
  if (!graphNodes.has("$START")) {
98
98
  errors.push("$START must be defined in graph");
99
99
  } else {
100
- const startKeys = Object.keys(payload.graph.$START);
101
- if (startKeys.length !== 1 || startKeys[0] !== "_") {
102
- errors.push('$START must have exactly one edge with status "_"');
100
+ const startKeys = new Set(Object.keys(payload.graph.$START));
101
+ if (!startKeys.has("new") || !startKeys.has("resume")) {
102
+ errors.push('$START must have edges with statuses "new" and "resume"');
103
103
  }
104
104
  }
105
105
 
@@ -190,22 +190,13 @@ function checkOneOfDiscriminant(
190
190
  }
191
191
  }
192
192
 
193
- /** Check status-edge consistency for a user role. "_" is reserved for $START and rejected here. */
193
+ /** Check status-edge consistency for a user role. */
194
194
  function checkStatusEdges(
195
195
  roleName: string,
196
196
  graphKeys: Set<string>,
197
197
  statusSet: Set<string>,
198
198
  errors: string[],
199
199
  ): void {
200
- if (graphKeys.has("_")) {
201
- errors.push(`role "${roleName}" must use explicit $status keys in graph, not "_"`);
202
- return;
203
- }
204
- if (statusSet.has("_")) {
205
- errors.push(`role "${roleName}" $status enum must use explicit values, not "_"`);
206
- return;
207
- }
208
-
209
200
  const extraKeys = [...graphKeys].filter((k) => !statusSet.has(k));
210
201
  const missingKeys = [...statusSet].filter((k) => !graphKeys.has(k));
211
202
  if (extraKeys.length > 0) {
package/src/validate.ts CHANGED
@@ -57,13 +57,13 @@ function isGraph(value: unknown): boolean {
57
57
  if (!isRecord(value)) {
58
58
  return false;
59
59
  }
60
- return Object.entries(value).every(([node, statusMap]) => {
60
+ return Object.values(value).every((statusMap) => {
61
61
  if (!isRecord(statusMap)) {
62
62
  return false;
63
63
  }
64
64
  return Object.entries(statusMap).every(([status, target]) => {
65
- // "_" is only valid as a status key for the $START entry node.
66
- if (status === "_" && node !== "$START") {
65
+ // "_" is no longer a valid status key anywhere $START uses "new"/"resume".
66
+ if (status === "_") {
67
67
  return false;
68
68
  }
69
69
  return isTarget(target);