wogiflow 2.6.1 → 2.6.3

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.
@@ -247,7 +247,14 @@ function processSuggestedTask(workspaceRoot, message) {
247
247
 
248
248
  try {
249
249
  const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
250
- const taskId = 'wf-' + crypto.randomBytes(4).toString('hex');
250
+ // Use generateTaskId when available, fallback to random (finding-008)
251
+ let taskId;
252
+ try {
253
+ const { generateTaskId } = require('../scripts/flow-utils');
254
+ taskId = generateTaskId(message.suggestedTask.title || message.subject);
255
+ } catch (_err) {
256
+ taskId = 'wf-' + crypto.randomBytes(4).toString('hex');
257
+ }
251
258
 
252
259
  const task = {
253
260
  id: taskId,
package/lib/workspace.js CHANGED
@@ -22,6 +22,22 @@ const path = require('node:path');
22
22
  // Constants
23
23
  // ============================================================
24
24
 
25
+ // Proto-pollution safe JSON parse (finding-007)
26
+ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
27
+ function safeParseJson(str, fallback) {
28
+ try {
29
+ const obj = JSON.parse(str);
30
+ if (obj && typeof obj === 'object') {
31
+ for (const key of Object.keys(obj)) {
32
+ if (DANGEROUS_KEYS.has(key)) delete obj[key];
33
+ }
34
+ }
35
+ return obj;
36
+ } catch (_err) {
37
+ return fallback;
38
+ }
39
+ }
40
+
25
41
  const WORKSPACE_CONFIG_FILE = 'wogi-workspace.json';
26
42
  const WORKSPACE_DIR = '.workspace';
27
43
  const WORKSPACE_DIRS = [
@@ -102,7 +118,7 @@ function readMemberMetadata(workflowPath) {
102
118
  try {
103
119
  if (fs.existsSync(filePath)) {
104
120
  const content = fs.readFileSync(filePath, 'utf-8');
105
- metadata[key] = json ? JSON.parse(content) : content;
121
+ metadata[key] = json ? safeParseJson(content, {}) : content;
106
122
  }
107
123
  } catch (_err) {
108
124
  // Non-critical — skip unreadable files
@@ -114,7 +130,7 @@ function readMemberMetadata(workflowPath) {
114
130
  const filePath = path.join(statePath, file);
115
131
  try {
116
132
  if (fs.existsSync(filePath)) {
117
- metadata[key] = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
133
+ metadata[key] = safeParseJson(fs.readFileSync(filePath, 'utf-8'), {});
118
134
  }
119
135
  } catch (_err) {
120
136
  // Non-critical
@@ -227,7 +243,7 @@ function detectStack(metadata, memberPath) {
227
243
  const pkgPath = path.join(memberPath, 'package.json');
228
244
  if (fs.existsSync(pkgPath)) {
229
245
  try {
230
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
246
+ const pkg = safeParseJson(fs.readFileSync(pkgPath, 'utf-8'), {});
231
247
  if (pkg.dependencies?.typescript || pkg.devDependencies?.typescript) {
232
248
  stack.language = 'TypeScript';
233
249
  } else {
@@ -516,12 +532,39 @@ function generateWorkspaceClaudeMd(config, manifest) {
516
532
 
517
533
  You are a **workspace manager** coordinating ${memberNames.length} repositories. You do NOT read source code directly. You read WogiFlow state files to understand each repo, then delegate implementation to repo-scoped sub-agents.
518
534
 
519
- ## CRITICAL RULES
520
-
521
- 1. **NEVER read source code** in member repos directly. Read \`.workflow/state/\` files (api-map.md, app-map.md, decisions.md) for context.
522
- 2. **ALWAYS delegate implementation** to sub-agents. You plan and coordinate — sub-agents write code.
523
- 3. **Provider before consumer.** When both need changes, implement the provider side first.
524
- 4. **Write messages after cross-repo changes.** Every change that affects another repo gets a message in \`.workspace/messages/\`.
535
+ ## CRITICAL RULES — YOU ARE A MANAGER, NOT A WORKER
536
+
537
+ **You are an orchestrator. You do NOT do implementation work. Ever.**
538
+
539
+ ### What you ARE allowed to do:
540
+ - Read \`.workflow/state/\` files (api-map.md, app-map.md, decisions.md, ready.json, schema-map.md)
541
+ - Read \`.workspace/\` files (messages, contracts, manifests)
542
+ - Dispatch tasks to workers via \`curl -s -X POST http://localhost:{port}\`
543
+ - Check worker health via \`curl -s http://localhost:{port}/health\`
544
+ - Read \`package.json\` from member repos (for stack/version info)
545
+ - Run \`git log\`, \`git status\`, \`git diff\` in member repos (to understand recent changes)
546
+ - Synthesize and present worker results to the user
547
+
548
+ ### What you are FORBIDDEN from doing:
549
+ - **Read source code** — NO reading \`src/\`, \`lib/\`, \`app/\`, \`components/\`, \`pages/\`, \`services/\`, \`modules/\` or ANY code files. If you need to know what code does, dispatch an investigation to a worker.
550
+ - **Edit or Write files** — NO editing any file in any member repo. If something needs to change, dispatch it to a worker.
551
+ - **Run build commands** — NO \`npm run build\`, \`vite build\`, \`tsc\`, \`nest build\`, etc. Dispatch to the worker.
552
+ - **Run deploy commands** — NO \`aws s3 sync\`, \`docker push\`, \`vercel deploy\`, etc. Dispatch to the worker.
553
+ - **Run tests** — NO \`npm test\`, \`jest\`, \`vitest\`, etc. Dispatch to the worker.
554
+ - **Use the Agent tool** — NO spawning sub-agents for investigation or implementation. Use channel dispatch instead.
555
+ - **Install packages** — NO \`npm install\`, \`yarn add\`, etc. Dispatch to the worker.
556
+
557
+ ### Anti-rationalization checklist:
558
+ If ANY of these thoughts cross your mind, you are about to violate role boundaries:
559
+ - "Let me just quickly check this one file" → WRONG. Dispatch to a worker.
560
+ - "I can see the fix is simple, let me just do it" → WRONG. Dispatch to a worker.
561
+ - "The worker is busy, I'll handle it myself" → WRONG. Wait or tell the user.
562
+ - "I need to understand the code to route correctly" → Read \`.workflow/state/\` files, NOT source code.
563
+ - "I'll investigate while waiting for the worker" → WRONG. Only workers investigate code.
564
+
565
+ ### Coordination rules:
566
+ 1. **Provider before consumer.** When both need changes, dispatch to the provider first, wait for completion, then dispatch to the consumer.
567
+ 2. **Write messages after cross-repo changes.** Every change that affects another repo gets a message in \`.workspace/messages/\`.
525
568
 
526
569
  ## Member Repos
527
570
 
@@ -910,10 +953,24 @@ function generateMemberMcpConfigs(workspaceRoot, config) {
910
953
  continue;
911
954
  }
912
955
 
956
+ // Reserved name check (finding-003) — 'manager' is used as the orchestrator identity
957
+ if (name === 'manager') {
958
+ console.error(` ✗ ${name}: 'manager' is a reserved name — rename this member repo`);
959
+ continue;
960
+ }
961
+
913
962
  const memberPath = path.resolve(workspaceRoot, config.members[name]?.path || `./${name}`);
914
963
 
915
- // Path traversal guard ensure member path is inside workspace root
916
- if (!memberPath.startsWith(workspaceRoot + path.sep) && memberPath !== workspaceRoot) {
964
+ // Path traversal guard with symlink resolution (finding-006)
965
+ let realRoot, realMember;
966
+ try {
967
+ realRoot = fs.realpathSync(workspaceRoot);
968
+ realMember = fs.realpathSync(memberPath);
969
+ } catch (_err) {
970
+ realRoot = workspaceRoot;
971
+ realMember = memberPath;
972
+ }
973
+ if (!realMember.startsWith(realRoot + path.sep) && realMember !== realRoot) {
917
974
  console.error(` ✗ ${name}: path escapes workspace root (${config.members[name]?.path}) — skipping`);
918
975
  continue;
919
976
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -282,62 +282,8 @@ async function handleTaskCompleted(input) {
282
282
  } catch (_err) {
283
283
  // Non-critical - registry manager may not be available
284
284
  }
285
- // Write results back to workspace message bus if running as a workspace worker
286
- if (result.completed && process.env.WOGI_WORKSPACE_ROOT) {
287
- try {
288
- const workspaceRoot = process.env.WOGI_WORKSPACE_ROOT;
289
- const repoName = process.env.WOGI_REPO_NAME || path.basename(process.cwd());
290
- const messagesDir = path.join(workspaceRoot, '.workspace', 'messages');
291
- fs.mkdirSync(messagesDir, { recursive: true });
292
-
293
- // Build a summary from available task data
294
- const summary = [];
295
- if (completedTask.title) summary.push(`**Task**: ${completedTask.title}`);
296
- if (completedTask.type) summary.push(`**Type**: ${completedTask.type}`);
297
- if (input.changedFiles?.length) summary.push(`**Files changed**: ${input.changedFiles.join(', ')}`);
298
- if (input.summary) summary.push(`**Summary**: ${input.summary}`);
299
-
300
- // Read the last request-log entry for richer context
301
- try {
302
- const logPath = path.join(PATHS.root, 'request-log.md');
303
- if (fs.existsSync(logPath)) {
304
- const logContent = fs.readFileSync(logPath, 'utf-8');
305
- const lastEntry = logContent.split(/^### R-/m).pop();
306
- if (lastEntry && lastEntry.length < 2000) {
307
- summary.push(`**Log entry**:\n${lastEntry.trim()}`);
308
- }
309
- }
310
- } catch (_err) {
311
- // Non-critical
312
- }
313
-
314
- const message = {
315
- id: 'msg-' + require('node:crypto').randomBytes(4).toString('hex'),
316
- from: repoName,
317
- to: 'manager',
318
- type: 'task-complete',
319
- priority: 'medium',
320
- timestamp: new Date().toISOString(),
321
- subject: `Task completed: ${completedTask.title || completedTask.id}`,
322
- body: summary.join('\n') || `Task ${completedTask.id} completed successfully.`,
323
- taskId: completedTask.id,
324
- actionRequired: false,
325
- status: 'pending'
326
- };
327
-
328
- const msgPath = path.join(messagesDir, `${message.id}.json`);
329
- fs.writeFileSync(msgPath, JSON.stringify(message, null, 2));
330
-
331
- if (process.env.DEBUG) {
332
- console.error(`[Task Completed] Workspace message written: ${msgPath}`);
333
- }
334
- } catch (err) {
335
- // Non-critical — workspace messaging is best-effort
336
- if (process.env.DEBUG) {
337
- console.error(`[Task Completed] Workspace message failed: ${err.message}`);
338
- }
339
- }
340
- }
285
+ // Workspace notifications are handled by the Stop hook (via HTTP to manager port).
286
+ // Removed duplicate file-based notification here to prevent double messages (finding-004).
341
287
 
342
288
  // Check pending queue — notify user if items are waiting
343
289
  try {
@@ -62,21 +62,24 @@ runHook('Stop', async ({ parsedInput }) => {
62
62
  }
63
63
 
64
64
  // Workspace worker: send results to manager via HTTP when stopping.
65
- // Uses synchronous curl to guarantee delivery before the hook process exits.
66
- // The async http.request approach was unreliable — process exited before request completed.
65
+ // Uses execFileSync with array args to avoid shell injection (finding-001).
67
66
  if (process.env.WOGI_MANAGER_PORT && process.env.WOGI_REPO_NAME && process.env.WOGI_REPO_NAME !== 'manager') {
68
67
  try {
69
- const { execSync } = require('node:child_process');
70
- const fs = require('node:fs');
68
+ const { execFileSync, execSync } = require('node:child_process');
71
69
  const path = require('node:path');
70
+ const VALID_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
72
71
  const repoName = process.env.WOGI_REPO_NAME;
73
- const managerPort = process.env.WOGI_MANAGER_PORT;
72
+ const managerPort = parseInt(process.env.WOGI_MANAGER_PORT, 10);
73
+
74
+ // Validate inputs before using them (finding-001, finding-002)
75
+ if (!VALID_NAME.test(repoName) || !Number.isInteger(managerPort) || managerPort < 1024 || managerPort > 65535) {
76
+ throw new Error(`Invalid WOGI_REPO_NAME or WOGI_MANAGER_PORT`);
77
+ }
74
78
 
75
79
  // Build summary from available state
76
80
  const summaryParts = [];
77
81
  const { PATHS, safeJsonParse } = require('../../flow-utils');
78
82
 
79
- // Get current/recently completed task info
80
83
  const ready = safeJsonParse(path.join(PATHS.state, 'ready.json'), {});
81
84
  const recentTask = (ready.recentlyCompleted || [])[0];
82
85
  const inProgressTask = (ready.inProgress || [])[0];
@@ -87,7 +90,6 @@ runHook('Stop', async ({ parsedInput }) => {
87
90
  if (task.type) summaryParts.push(`**Type**: ${task.type}`);
88
91
  }
89
92
 
90
- // Get changed files
91
93
  try {
92
94
  const diff = execSync('git diff --name-only HEAD 2>/dev/null || true', { cwd: PATHS.root, encoding: 'utf-8' }).trim();
93
95
  const staged = execSync('git diff --name-only --staged 2>/dev/null || true', { cwd: PATHS.root, encoding: 'utf-8' }).trim();
@@ -97,22 +99,19 @@ runHook('Stop', async ({ parsedInput }) => {
97
99
  }
98
100
  } catch (_err) { /* non-critical */ }
99
101
 
100
- const body = summaryParts.join('\n') || `${repoName} finished processing.`;
102
+ const body = summaryParts.join('\n') || `Work completed by ${repoName}.`;
101
103
 
102
- // PRIMARY: Synchronous curl to manager port guaranteed to complete before exit
104
+ // execFileSync with array argsno shell interpretation (finding-001 fix)
103
105
  try {
104
- execSync(
105
- `curl -s -X POST http://127.0.0.1:${managerPort} -H "Content-Type: text/plain" -H "X-Wogi-From: ${repoName}" --data-binary @-`,
106
- { input: body, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
107
- );
108
- if (process.env.DEBUG) {
109
- console.error(`[Stop] Sent results to manager via curl :${managerPort}`);
110
- }
106
+ execFileSync('curl', [
107
+ '-s', '-X', 'POST',
108
+ `http://127.0.0.1:${managerPort}`,
109
+ '-H', 'Content-Type: text/plain',
110
+ '-H', `X-Wogi-From: ${repoName}`,
111
+ '--data-binary', '@-'
112
+ ], { input: body, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
111
113
  } catch (_err) {
112
114
  // Manager might be offline — that's OK
113
- if (process.env.DEBUG) {
114
- console.error(`[Stop] curl to manager:${managerPort} failed: ${_err.message}`);
115
- }
116
115
  }
117
116
  } catch (err) {
118
117
  if (process.env.DEBUG) {