@e0ipso/ai-task-manager 1.18.7 → 1.20.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.
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Runtime Prompt Composition Script
5
+ *
6
+ * Reads markdown command files, performs variable substitution, and outputs
7
+ * composed prompts to enable uninterrupted workflow execution.
8
+ *
9
+ * Usage:
10
+ * node compose-prompt.cjs --template "create-plan.md" --variable "ARGUMENTS=user input" --variable "1=51"
11
+ *
12
+ * Arguments:
13
+ * --template: Relative path to template file (e.g., "create-plan.md")
14
+ * --variable: Key=value pairs for substitution (can be specified multiple times)
15
+ *
16
+ * Output:
17
+ * Composed markdown prompt to stdout with variables substituted
18
+ *
19
+ * Exit Codes:
20
+ * 0: Success
21
+ * 1: Error (with message to stderr)
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ /**
28
+ * Parse command line arguments
29
+ * @returns {Object} Parsed arguments with template and variables
30
+ */
31
+ function parseArguments() {
32
+ const args = process.argv.slice(2);
33
+ const result = {
34
+ template: null,
35
+ variables: {}
36
+ };
37
+
38
+ for (let i = 0; i < args.length; i++) {
39
+ if (args[i] === '--template') {
40
+ if (i + 1 >= args.length) {
41
+ throw new Error('--template requires a value');
42
+ }
43
+ result.template = args[++i];
44
+ } else if (args[i] === '--variable') {
45
+ if (i + 1 >= args.length) {
46
+ throw new Error('--variable requires a value');
47
+ }
48
+ const varArg = args[++i];
49
+ const eqIndex = varArg.indexOf('=');
50
+ if (eqIndex === -1) {
51
+ throw new Error(`Invalid variable format: ${varArg}. Expected format: key=value`);
52
+ }
53
+ const key = varArg.substring(0, eqIndex);
54
+ const value = varArg.substring(eqIndex + 1);
55
+ result.variables[key] = value;
56
+ } else {
57
+ throw new Error(`Unknown argument: ${args[i]}`);
58
+ }
59
+ }
60
+
61
+ if (!result.template) {
62
+ throw new Error('--template argument is required');
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ /**
69
+ * Find the template file in the appropriate directory
70
+ * Priority: .claude/commands/tasks/ then templates/assistant/commands/tasks/
71
+ * @param {string} templateName - Relative path to template file
72
+ * @returns {string} Absolute path to template file
73
+ */
74
+ function findTemplateFile(templateName) {
75
+ // Get the root directory (assuming script is in templates/ai-task-manager/config/scripts/)
76
+ const scriptDir = __dirname;
77
+ const rootDir = path.resolve(scriptDir, '../../../..');
78
+
79
+ // Define search paths in priority order
80
+ const searchPaths = [
81
+ path.join(rootDir, '.claude', 'commands', 'tasks', templateName),
82
+ path.join(rootDir, 'templates', 'assistant', 'commands', 'tasks', templateName)
83
+ ];
84
+
85
+ for (const searchPath of searchPaths) {
86
+ if (fs.existsSync(searchPath)) {
87
+ return searchPath;
88
+ }
89
+ }
90
+
91
+ throw new Error(`Template not found: ${templateName}`);
92
+ }
93
+
94
+ /**
95
+ * Parse YAML frontmatter from markdown content
96
+ * Based on the parseFrontmatter function from src/utils.ts
97
+ * @param {string} content - The markdown content with frontmatter
98
+ * @returns {Object} Object containing frontmatter and body content
99
+ */
100
+ function parseFrontmatter(content) {
101
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/;
102
+ const match = content.match(frontmatterRegex);
103
+
104
+ if (!match) {
105
+ return {
106
+ frontmatter: {},
107
+ body: content
108
+ };
109
+ }
110
+
111
+ const frontmatterContent = match[1] || '';
112
+ const bodyContent = match[2] || '';
113
+
114
+ // Simple YAML parser for our specific use case
115
+ const frontmatter = {};
116
+ const lines = frontmatterContent.split('\n');
117
+
118
+ for (const line of lines) {
119
+ const trimmed = line.trim();
120
+ if (!trimmed || trimmed.startsWith('#')) continue;
121
+
122
+ const colonIndex = trimmed.indexOf(':');
123
+ if (colonIndex === -1) continue;
124
+
125
+ const key = trimmed.substring(0, colonIndex).trim();
126
+ const value = trimmed.substring(colonIndex + 1).trim();
127
+
128
+ // Remove quotes if present
129
+ frontmatter[key] = value.replace(/^["']|["']$/g, '');
130
+ }
131
+
132
+ return {
133
+ frontmatter,
134
+ body: bodyContent
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Perform variable substitution in the template body
140
+ * Replaces $ARGUMENTS, $1, $2, etc. with provided values
141
+ * Does NOT process frontmatter variables (those stay as-is)
142
+ * @param {string} body - The template body content
143
+ * @param {Object} variables - Key-value pairs for substitution
144
+ * @returns {string} Body content with substitutions applied
145
+ */
146
+ function substituteVariables(body, variables) {
147
+ let result = body;
148
+
149
+ // Replace $ARGUMENTS if provided
150
+ if (variables.ARGUMENTS !== undefined) {
151
+ // Use regex with negative lookahead to avoid replacing $ARGUMENTS that are part of longer identifiers
152
+ result = result.replace(/\$ARGUMENTS(?![0-9A-Za-z_])/g, variables.ARGUMENTS);
153
+ }
154
+
155
+ // Replace positional arguments ($1, $2, $3, etc.)
156
+ // Use regex with negative lookahead to avoid replacing $1 that's part of $10, etc.
157
+ for (const key in variables) {
158
+ if (/^\d+$/.test(key)) {
159
+ const pattern = new RegExp(`\\$${key}(?![0-9])`, 'g');
160
+ result = result.replace(pattern, variables[key]);
161
+ }
162
+ }
163
+
164
+ return result;
165
+ }
166
+
167
+ /**
168
+ * Reconstruct markdown content with frontmatter
169
+ * @param {Object} frontmatter - The frontmatter object
170
+ * @param {string} body - The body content
171
+ * @returns {string} Complete markdown content
172
+ */
173
+ function reconstructMarkdown(frontmatter, body) {
174
+ if (Object.keys(frontmatter).length === 0) {
175
+ return body;
176
+ }
177
+
178
+ let result = '---\n';
179
+ for (const [key, value] of Object.entries(frontmatter)) {
180
+ // Preserve original formatting - add quotes if value contains special characters
181
+ if (typeof value === 'string' && (value.includes(':') || value.includes('[') || value.includes(']'))) {
182
+ result += `${key}: "${value}"\n`;
183
+ } else {
184
+ result += `${key}: ${value}\n`;
185
+ }
186
+ }
187
+ result += '---\n';
188
+
189
+ if (body) {
190
+ result += body;
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ /**
197
+ * Main execution function
198
+ */
199
+ function main() {
200
+ try {
201
+ // Parse command line arguments
202
+ const { template, variables } = parseArguments();
203
+
204
+ // Find the template file
205
+ const templatePath = findTemplateFile(template);
206
+
207
+ // Read the template file
208
+ const content = fs.readFileSync(templatePath, 'utf-8');
209
+
210
+ // Parse frontmatter and body
211
+ const { frontmatter, body } = parseFrontmatter(content);
212
+
213
+ // Perform variable substitution on the body only
214
+ const substitutedBody = substituteVariables(body, variables);
215
+
216
+ // Reconstruct the markdown with frontmatter preserved
217
+ const output = reconstructMarkdown(frontmatter, substitutedBody);
218
+
219
+ // Output to stdout
220
+ process.stdout.write(output);
221
+
222
+ // Exit successfully
223
+ process.exit(0);
224
+ } catch (error) {
225
+ // Write error to stderr
226
+ process.stderr.write(`Error: ${error.message}\n`);
227
+
228
+ // Exit with error code
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ // Execute main function
234
+ main();
@@ -233,17 +233,22 @@ function listAvailablePlans() {
233
233
  }
234
234
 
235
235
  /**
236
- * Validate plan blueprint and output JSON
236
+ * Validate plan blueprint and output JSON or specific field
237
237
  * @param {string|number} planId - Plan ID to validate
238
+ * @param {string} [fieldName] - Optional field name to extract (planFile, planDir, taskCount, blueprintExists)
238
239
  */
239
- function validatePlanBlueprint(planId) {
240
+ function validatePlanBlueprint(planId, fieldName) {
240
241
  if (!planId) {
241
242
  errorLog('Plan ID is required');
242
243
  errorLog('');
243
- errorLog('Usage: node validate-plan-blueprint.cjs <plan-id>');
244
+ errorLog('Usage: node validate-plan-blueprint.cjs <plan-id> [field-name]');
244
245
  errorLog('');
245
- errorLog('Example:');
246
- errorLog(' node validate-plan-blueprint.cjs 47');
246
+ errorLog('Examples:');
247
+ errorLog(' node validate-plan-blueprint.cjs 47 # Output full JSON');
248
+ errorLog(' node validate-plan-blueprint.cjs 47 planFile # Output just the plan file path');
249
+ errorLog(' node validate-plan-blueprint.cjs 47 planDir # Output just the plan directory');
250
+ errorLog(' node validate-plan-blueprint.cjs 47 taskCount # Output just the task count');
251
+ errorLog(' node validate-plan-blueprint.cjs 47 blueprintExists # Output yes/no');
247
252
  process.exit(1);
248
253
  }
249
254
 
@@ -282,13 +287,27 @@ function validatePlanBlueprint(planId) {
282
287
  planFile,
283
288
  planDir,
284
289
  taskCount,
285
- blueprintExists
290
+ blueprintExists: blueprintExists ? 'yes' : 'no'
286
291
  };
287
292
 
288
293
  debugLog('Validation complete:', result);
289
- console.log(JSON.stringify(result, null, 2));
294
+
295
+ // If field name is provided, output just that field
296
+ if (fieldName) {
297
+ const validFields = ['planFile', 'planDir', 'taskCount', 'blueprintExists'];
298
+ if (!validFields.includes(fieldName)) {
299
+ errorLog(`Invalid field name: ${fieldName}`);
300
+ errorLog(`Valid fields: ${validFields.join(', ')}`);
301
+ process.exit(1);
302
+ }
303
+ console.log(result[fieldName]);
304
+ } else {
305
+ // Output full JSON for backward compatibility
306
+ console.log(JSON.stringify(result, null, 2));
307
+ }
290
308
  }
291
309
 
292
310
  // Main execution
293
311
  const planId = process.argv[2];
294
- validatePlanBlueprint(planId);
312
+ const fieldName = process.argv[3];
313
+ validatePlanBlueprint(planId, fieldName);
@@ -2,8 +2,6 @@
2
2
  id: [PLAN-ID]
3
3
  summary: "[Brief one-line description of what this plan accomplishes]"
4
4
  created: [YYYY-MM-DD]
5
- approval_method_plan: [auto|manual] # Workflow approval for plan review (default: manual)
6
- approval_method_tasks: [auto|manual] # Workflow approval for task generation review (default: manual)
7
5
  ---
8
6
 
9
7
  # Plan: [Descriptive Plan Title]
@@ -83,17 +83,15 @@ Only after confirming sufficient context, create a plan that includes:
83
83
 
84
84
  Remember that a plan needs to be reviewed by a human. Be concise and to the point. Also, include mermaid diagrams to illustrate the plan.
85
85
 
86
- ##### Output Format
87
- Structure your response as follows:
88
- - If context is insufficient: List specific clarifying questions
89
- - If context is sufficient: Provide the comprehensive plan using the structure above. Use the information in @TASK_MANAGER.md for the directory structure and additional information about plans.
86
+ ##### CRITICAL: Output Format
90
87
 
91
88
  **Output Behavior: CRITICAL - Structured Output for Command Coordination**
92
89
 
93
- Always end your output with a standardized summary in this exact format:
90
+ Always end your output with a standardized summary in this exact format, for command coordination:
94
91
 
95
92
  ```
96
93
  ---
94
+
97
95
  Plan Summary:
98
96
  - Plan ID: [numeric-id]
99
97
  - Plan File: [full-path-to-plan-file]
@@ -118,13 +116,9 @@ Example:
118
116
  id: 1
119
117
  summary: "Implement a comprehensive CI/CD pipeline using GitHub Actions for automated linting, testing, semantic versioning, and NPM publishing"
120
118
  created: 2025-09-01
121
- approval_method_plan: "manual"
122
- approval_method_tasks: "manual"
123
119
  ---
124
120
  ```
125
121
 
126
- **Important**: Always set both `approval_method_plan` and `approval_method_tasks` to "manual" when creating a plan. The full-workflow command will modify these fields to "auto" after creation if running in automated mode.
127
-
128
122
  The schema for this frontmatter is:
129
123
  ```json
130
124
  {
@@ -143,16 +137,6 @@ The schema for this frontmatter is:
143
137
  "type": "string",
144
138
  "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
145
139
  "description": "Creation date in YYYY-MM-DD format"
146
- },
147
- "approval_method_plan": {
148
- "type": "string",
149
- "enum": ["auto", "manual"],
150
- "description": "Workflow approval mode for plan review: auto for automated workflows, manual for standalone execution"
151
- },
152
- "approval_method_tasks": {
153
- "type": "string",
154
- "enum": ["auto", "manual"],
155
- "description": "Workflow approval mode for task generation review: auto when tasks auto-generated in workflow, manual for standalone execution"
156
140
  }
157
141
  },
158
142
  "additionalProperties": false
@@ -169,19 +153,3 @@ node .ai/task-manager/config/scripts/get-next-plan-id.cjs
169
153
  **Key formatting:**
170
154
  - **Front-matter**: Use numeric values (`id: 7`)
171
155
  - **Directory names**: Use zero-padded strings (`07--plan-name`)
172
-
173
- This Node.js script provides robust plan ID generation with comprehensive error handling:
174
-
175
- **Features:**
176
- - **Flexible Whitespace Handling**: Supports various frontmatter patterns
177
- - **Validation Layer**: Only processes files with valid numeric ID fields in YAML frontmatter
178
- - **Error Resilience**: Gracefully handles empty directories, corrupted files, and parsing failures
179
- - **Fallback Logic**: Returns ID 1 when no valid plans found, ensuring script never fails
180
- - **Dual ID Detection**: Checks both filename patterns and frontmatter for maximum reliability
181
-
182
- **Handles Edge Cases:**
183
- - Empty plans/archive directories → Returns 1
184
- - Corrupted or malformed YAML frontmatter → Skips invalid files
185
- - Non-numeric ID values → Filters out automatically
186
- - Missing frontmatter → Uses filename fallback
187
- - File system errors → Gracefully handled