agent-state-machine 2.0.13 → 2.0.15

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 (36) hide show
  1. package/README.md +19 -6
  2. package/bin/cli.js +23 -5
  3. package/lib/setup.js +82 -388
  4. package/package.json +2 -1
  5. package/templates/project-builder/README.md +119 -0
  6. package/templates/project-builder/agents/assumptions-clarifier.md +66 -0
  7. package/templates/project-builder/agents/code-reviewer.md +82 -0
  8. package/templates/project-builder/agents/code-writer.md +75 -0
  9. package/templates/project-builder/agents/requirements-clarifier.md +56 -0
  10. package/templates/project-builder/agents/roadmap-generator.md +74 -0
  11. package/templates/project-builder/agents/scope-clarifier.md +45 -0
  12. package/templates/project-builder/agents/security-clarifier.md +72 -0
  13. package/templates/project-builder/agents/security-reviewer.md +72 -0
  14. package/templates/project-builder/agents/task-planner.md +63 -0
  15. package/templates/project-builder/agents/test-planner.md +77 -0
  16. package/templates/project-builder/config.js +13 -0
  17. package/templates/project-builder/scripts/mac-notification.js +24 -0
  18. package/templates/project-builder/scripts/text-human.js +92 -0
  19. package/templates/project-builder/scripts/workflow-helpers.js +167 -0
  20. package/templates/project-builder/state/current.json +9 -0
  21. package/templates/project-builder/state/history.jsonl +0 -0
  22. package/templates/project-builder/steering/config.json +5 -0
  23. package/templates/project-builder/steering/global.md +19 -0
  24. package/templates/project-builder/workflow.js +394 -0
  25. package/templates/starter/README.md +118 -0
  26. package/templates/starter/agents/example.js +36 -0
  27. package/templates/starter/agents/yoda-greeter.md +12 -0
  28. package/templates/starter/agents/yoda-name-collector.md +12 -0
  29. package/templates/starter/config.js +12 -0
  30. package/templates/starter/interactions/.gitkeep +0 -0
  31. package/templates/starter/scripts/mac-notification.js +24 -0
  32. package/templates/starter/state/current.json +9 -0
  33. package/templates/starter/state/history.jsonl +0 -0
  34. package/templates/starter/steering/config.json +5 -0
  35. package/templates/starter/steering/global.md +19 -0
  36. package/templates/starter/workflow.js +52 -0
@@ -0,0 +1,63 @@
1
+ ---
2
+ model: high
3
+ output: result
4
+ format: json
5
+ ---
6
+
7
+ # Task Planner Agent
8
+
9
+ You are a task breakdown specialist. Generate detailed task lists for a specific phase as structured JSON.
10
+
11
+ ## Context
12
+ Project Description: {{projectDescription}}
13
+ Scope: {{scope}}
14
+ Requirements: {{requirements}}
15
+ Phase Number: {{phaseIndex}}
16
+ Phase Details: {{phase}}
17
+ {{#if feedback}}
18
+ User Feedback: {{feedback}}
19
+ {{/if}}
20
+
21
+ ## Instructions
22
+
23
+ Break down the phase into specific, actionable tasks. Each task should:
24
+ - Be small enough to complete in a focused work session
25
+ - Have a clear definition of done
26
+ - Include a sanity check the user can verify
27
+
28
+ **Task Principles:**
29
+ - One task = one concern (don't combine unrelated work)
30
+ - Tasks should be independently verifiable
31
+ - Order tasks by dependency (what must come first)
32
+ - Include setup/preparation tasks if needed
33
+
34
+ ## Output Format
35
+
36
+ Return a valid JSON object (no markdown code blocks, just raw JSON):
37
+
38
+ {
39
+ "phaseNumber": 1,
40
+ "phaseTitle": "Phase Title",
41
+ "tasks": [
42
+ {
43
+ "id": 1,
44
+ "title": "Task Title",
45
+ "description": "What needs to be done",
46
+ "doneDefinition": "Specific completion criteria that can be verified",
47
+ "sanityCheck": "How the user can verify this is working correctly",
48
+ "stage": "pending"
49
+ },
50
+ {
51
+ "id": 2,
52
+ "title": "Task Title",
53
+ "description": "What needs to be done",
54
+ "doneDefinition": "Specific completion criteria",
55
+ "sanityCheck": "Verification method",
56
+ "stage": "pending"
57
+ }
58
+ ]
59
+ }
60
+
61
+ **Stage values:** pending, in_progress, completed, failed
62
+
63
+ Keep tasks focused and achievable. Aim for 3-8 tasks per phase depending on complexity. Every task MUST have a doneDefinition and sanityCheck.
@@ -0,0 +1,77 @@
1
+ ---
2
+ model: med
3
+ output: result
4
+ format: json
5
+ ---
6
+
7
+ # Test Planner Agent
8
+
9
+ You are a test planning specialist. Create test plans for tasks before implementation.
10
+
11
+ ## Context
12
+ Task: {{task}}
13
+ Phase: {{phase}}
14
+ Requirements: {{requirements}}
15
+ Security Considerations: {{securityConsiderations}}
16
+ {{#if feedback}}
17
+ Previous Feedback: {{feedback}}
18
+ {{/if}}
19
+
20
+ ## Instructions
21
+
22
+ Create a comprehensive test plan for the task. Include:
23
+
24
+ **Test Categories:**
25
+ - Unit tests (individual functions/components)
26
+ - Integration tests (component interactions)
27
+ - Security tests (based on security review)
28
+ - Edge case tests (boundary conditions)
29
+
30
+ **Test Principles:**
31
+ - Test behavior, not implementation
32
+ - Cover happy path and error cases
33
+ - Include tests for security concerns flagged in review
34
+ - Prioritize tests by risk and importance
35
+
36
+ ## Output Format
37
+
38
+ Return a valid JSON object:
39
+
40
+ {
41
+ "testPlan": {
42
+ "summary": "Brief description of testing approach",
43
+ "unitTests": [
44
+ {
45
+ "name": "should validate user input",
46
+ "description": "Verify input sanitization works correctly",
47
+ "expectedBehavior": "Invalid input should be rejected with error message",
48
+ "priority": "high"
49
+ }
50
+ ],
51
+ "integrationTests": [
52
+ {
53
+ "name": "should save and retrieve data",
54
+ "description": "Verify database integration works",
55
+ "components": ["API", "Database"],
56
+ "priority": "high"
57
+ }
58
+ ],
59
+ "securityTests": [
60
+ {
61
+ "name": "should prevent SQL injection",
62
+ "threat": "SQL injection via user input",
63
+ "testMethod": "Attempt injection with malicious strings",
64
+ "priority": "high"
65
+ }
66
+ ],
67
+ "edgeCases": [
68
+ {
69
+ "scenario": "Empty input handling",
70
+ "expectedBehavior": "Return validation error"
71
+ }
72
+ ]
73
+ },
74
+ "testingNotes": "Any special considerations or setup needed"
75
+ }
76
+
77
+ Focus on tests that validate the definition of done. Don't over-test trivial functionality.
@@ -0,0 +1,13 @@
1
+ export const config = {
2
+ models: {
3
+ low: "gemini",
4
+ med: "gemini",
5
+ high: "gemini",
6
+ },
7
+ apiKeys: {
8
+ gemini: process.env.GEMINI_API_KEY,
9
+ anthropic: process.env.ANTHROPIC_API_KEY,
10
+ openai: process.env.OPENAI_API_KEY,
11
+ },
12
+ remotePath: "TczrLmUecnqZPpPhBTrvU374CGlfzDfINrr0eN0nMgQ",
13
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+
6
+ function escAppleScript(s) {
7
+ return String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
8
+ }
9
+
10
+ function notify(title = "Notification", message = "Everything finished!") {
11
+ const script = `display notification "${escAppleScript(message)}" with title "${escAppleScript(title)}"`;
12
+ spawnSync("osascript", ["-e", script], { stdio: "ignore" });
13
+
14
+ const soundPath = "/System/Library/Sounds/Glass.aiff";
15
+ const fallbackPath = "/System/Library/Sounds/Ping.aiff";
16
+
17
+ if (existsSync(soundPath)) {
18
+ spawnSync("afplay", [soundPath], { stdio: "ignore" });
19
+ } else if (existsSync(fallbackPath)) {
20
+ spawnSync("afplay", [fallbackPath], { stdio: "ignore" });
21
+ }
22
+ }
23
+
24
+ export { notify };
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import nodemailer from "nodemailer";
5
+
6
+ function loadEnvFile() {
7
+ if (typeof process.loadEnvFile === "function") {
8
+ process.loadEnvFile();
9
+ return;
10
+ }
11
+
12
+ const envPath = ".env";
13
+ if (!existsSync(envPath)) {
14
+ return;
15
+ }
16
+
17
+ const lines = readFileSync(envPath, "utf8").split(/\r?\n/);
18
+ for (const line of lines) {
19
+ const trimmed = line.trim();
20
+ if (!trimmed || trimmed.startsWith("#")) {
21
+ continue;
22
+ }
23
+
24
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
25
+ if (!match) {
26
+ continue;
27
+ }
28
+
29
+ const key = match[1];
30
+ if (process.env[key] !== undefined) {
31
+ continue;
32
+ }
33
+
34
+ let value = match[2] ?? "";
35
+ if (
36
+ (value.startsWith("\"") && value.endsWith("\"")) ||
37
+ (value.startsWith("'") && value.endsWith("'"))
38
+ ) {
39
+ value = value.slice(1, -1);
40
+ }
41
+
42
+ process.env[key] = value;
43
+ }
44
+ }
45
+
46
+ function requireEnv(name) {
47
+ const value = process.env[name];
48
+ if (!value) {
49
+ throw new Error(`Missing required env var: ${name}`);
50
+ }
51
+ return value;
52
+ }
53
+
54
+ function createTransport() {
55
+ const host = requireEnv("SMTP_HOST");
56
+ const port = Number(requireEnv("SMTP_PORT"));
57
+ const user = requireEnv("SMTP_USER");
58
+ const pass = requireEnv("SMTP_PASS");
59
+ const secure = String(process.env.SMTP_SECURE || "").toLowerCase() === "true";
60
+
61
+ return nodemailer.createTransport({
62
+ host,
63
+ port,
64
+ secure,
65
+ auth: {
66
+ user,
67
+ pass,
68
+ },
69
+ });
70
+ }
71
+
72
+ async function textHuman(message) {
73
+ loadEnvFile();
74
+
75
+ if (!message || typeof message !== "string") {
76
+ throw new Error("textHuman(message) requires a non-empty string.");
77
+ }
78
+
79
+ const from = process.env.SMS_FROM || requireEnv("SMTP_FROM");
80
+ const to = requireEnv("SMS_TO");
81
+ const transporter = createTransport();
82
+
83
+ const info = await transporter.sendMail({
84
+ from,
85
+ to,
86
+ subject: "",
87
+ text: message,
88
+ });
89
+ void info;
90
+ }
91
+
92
+ export { textHuman };
@@ -0,0 +1,167 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { agent, memory } from 'agent-state-machine';
4
+
5
+ // Normalize agent output - always extract the result consistently
6
+ function unwrap(agentResult) {
7
+ if (!agentResult) return null;
8
+ if (typeof agentResult === 'object' && 'result' in agentResult) {
9
+ return agentResult.result;
10
+ }
11
+ return agentResult;
12
+ }
13
+
14
+ // Write markdown file to workflow state directory
15
+ function writeMarkdownFile(stateDir, filename, content) {
16
+ if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
17
+ const filePath = path.join(stateDir, filename);
18
+ fs.writeFileSync(filePath, content);
19
+ console.log(` [File] Updated: ${filename}`);
20
+ return filePath;
21
+ }
22
+
23
+ // Strict approval parsing - only accepts explicit approval
24
+ function isApproval(response) {
25
+ if (!response || typeof response !== 'string') return false;
26
+ const trimmed = response.trim().toLowerCase();
27
+ // Must start with 'a' or be exactly 'approve/approved/yes/y'
28
+ return /^a\b/.test(trimmed) ||
29
+ /^approve/.test(trimmed) ||
30
+ /^yes\b/.test(trimmed) ||
31
+ /^y\b/.test(trimmed);
32
+ }
33
+
34
+ // Generate markdown from roadmap JSON
35
+ function renderRoadmapMarkdown(roadmap) {
36
+ if (!roadmap || !roadmap.phases) return '# Project Roadmap\n\nNo phases defined.';
37
+
38
+ let md = `# Project Roadmap: ${roadmap.title || 'Untitled Project'}\n\n`;
39
+
40
+ for (const phase of roadmap.phases) {
41
+ const status = phase.completed ? ' [COMPLETED]' : '';
42
+ md += `## Phase ${phase.number}: ${phase.title}${status}\n`;
43
+ md += `**Objective:** ${phase.objective || 'No objective specified'}\n\n`;
44
+
45
+ for (const item of phase.checklist || []) {
46
+ const check = item.completed ? 'x' : ' ';
47
+ md += `- [${check}] ${item.text}\n`;
48
+ }
49
+ md += '\n';
50
+ }
51
+
52
+ if (roadmap.notes && roadmap.notes.length > 0) {
53
+ md += '---\n\n**Notes:**\n';
54
+ for (const note of roadmap.notes) {
55
+ md += `- ${note}\n`;
56
+ }
57
+ }
58
+
59
+ return md;
60
+ }
61
+
62
+ // Generate markdown from tasks JSON
63
+ function renderTasksMarkdown(phaseNumber, phaseTitle, tasks) {
64
+ if (!tasks || !Array.isArray(tasks)) return `# Phase ${phaseNumber} Tasks\n\nNo tasks defined.`;
65
+
66
+ let md = `# Phase ${phaseNumber} Tasks: ${phaseTitle}\n\n`;
67
+
68
+ for (const task of tasks) {
69
+ const status = task.stage === 'completed' ? ' [COMPLETED]' :
70
+ task.stage === 'in_progress' ? ' [IN PROGRESS]' : '';
71
+ md += `## Task ${task.id}: ${task.title}${status}\n`;
72
+ md += `**Description:** ${task.description || 'No description'}\n\n`;
73
+ md += `**Definition of Done:**\n- ${task.doneDefinition || 'Task completed successfully'}\n\n`;
74
+ md += `**Sanity Check:**\n- ${task.sanityCheck || 'Review the implementation and confirm it meets requirements.'}\n\n`;
75
+ md += '---\n\n';
76
+ }
77
+
78
+ md += '## Checklist Summary\n';
79
+ for (const task of tasks) {
80
+ const check = task.stage === 'completed' ? 'x' : ' ';
81
+ md += `- [${check}] Task ${task.id}: ${task.title}\n`;
82
+ }
83
+
84
+ return md;
85
+ }
86
+
87
+ // Run agent with error handling and retry capability
88
+ async function safeAgent(name, params, options = {}) {
89
+ const { maxRetries = 1, onError } = options;
90
+ let lastError;
91
+
92
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
93
+ try {
94
+ const result = await agent(name, params);
95
+ return unwrap(result);
96
+ } catch (error) {
97
+ lastError = error;
98
+ console.error(` [Agent: ${name}] Error (attempt ${attempt}/${maxRetries}): ${error.message}`);
99
+
100
+ if (onError) {
101
+ const shouldRetry = await onError(error, attempt);
102
+ if (!shouldRetry) break;
103
+ }
104
+
105
+ if (attempt < maxRetries) {
106
+ console.log(` [Agent: ${name}] Retrying...`);
107
+ }
108
+ }
109
+ }
110
+
111
+ // Store error in memory for debugging
112
+ if (!memory._errors) memory._errors = [];
113
+ memory._errors.push({
114
+ agent: name,
115
+ error: lastError?.message,
116
+ timestamp: new Date().toISOString()
117
+ });
118
+
119
+ throw lastError;
120
+ }
121
+
122
+ // Task stage management
123
+ const TASK_STAGES = {
124
+ PENDING: 'pending',
125
+ SECURITY_PRE: 'security_pre',
126
+ TEST_PLANNING: 'test_planning',
127
+ IMPLEMENTING: 'implementing',
128
+ CODE_REVIEW: 'code_review',
129
+ SECURITY_POST: 'security_post',
130
+ AWAITING_APPROVAL: 'awaiting_approval',
131
+ COMPLETED: 'completed',
132
+ FAILED: 'failed'
133
+ };
134
+
135
+ function getTaskStage(phaseIndex, taskId) {
136
+ const key = `phase_${phaseIndex}_task_${taskId}_stage`;
137
+ return memory[key] || TASK_STAGES.PENDING;
138
+ }
139
+
140
+ function setTaskStage(phaseIndex, taskId, stage) {
141
+ const key = `phase_${phaseIndex}_task_${taskId}_stage`;
142
+ memory[key] = stage;
143
+ }
144
+
145
+ function getTaskData(phaseIndex, taskId, dataKey) {
146
+ const key = `phase_${phaseIndex}_task_${taskId}_${dataKey}`;
147
+ return memory[key];
148
+ }
149
+
150
+ function setTaskData(phaseIndex, taskId, dataKey, value) {
151
+ const key = `phase_${phaseIndex}_task_${taskId}_${dataKey}`;
152
+ memory[key] = value;
153
+ }
154
+
155
+ export {
156
+ unwrap,
157
+ writeMarkdownFile,
158
+ isApproval,
159
+ renderRoadmapMarkdown,
160
+ renderTasksMarkdown,
161
+ safeAgent,
162
+ TASK_STAGES,
163
+ getTaskStage,
164
+ setTaskStage,
165
+ getTaskData,
166
+ setTaskData
167
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "format": "native",
3
+ "status": "IDLE",
4
+ "memory": {},
5
+ "_pendingInteraction": null,
6
+ "_error": null,
7
+ "startedAt": null,
8
+ "lastUpdatedAt": "__NOW_ISO__"
9
+ }
File without changes
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Steering configuration",
3
+ "enabled": true,
4
+ "globalPrompt": "global.md"
5
+ }
@@ -0,0 +1,19 @@
1
+ # Global Steering Prompt
2
+
3
+ This content is included with every agent execution in the project-builder workflow.
4
+
5
+ ## Guidelines
6
+
7
+ - Process data carefully and validate inputs
8
+ - Return well-structured JSON when applicable
9
+ - Log meaningful progress messages
10
+ - Handle errors gracefully
11
+
12
+ ## Helpful Tips
13
+ - Scripts live in /workflows/project-builder/scripts/
14
+ - .gitignore includes /workflows so include ignored files when searching for a script and use `search_file_content` with the `no_ignore` flag to read the contents of script files.
15
+ - Shell execution via `run_shell_command` is unavailable, so reproduce the script's intended side effect to fulfill the task.
16
+
17
+ ## Notes
18
+
19
+ This file is automatically loaded and passed to every agent in the workflow via `context._steering.global`.