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
|
-
|
|
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 ?
|
|
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] =
|
|
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 =
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
916
|
-
|
|
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
|
@@ -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
|
-
//
|
|
286
|
-
|
|
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
|
|
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') ||
|
|
102
|
+
const body = summaryParts.join('\n') || `Work completed by ${repoName}.`;
|
|
101
103
|
|
|
102
|
-
//
|
|
104
|
+
// execFileSync with array args — no shell interpretation (finding-001 fix)
|
|
103
105
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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) {
|