wogiflow 2.20.1 → 2.22.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.
- package/.claude/commands/wogi-finalize.md +83 -0
- package/.claude/rules/_internal/self-maintenance.md +1 -1
- package/.claude/settings.json +1 -1
- package/lib/commands/login.js +1 -1
- package/lib/installer.js +5 -5
- package/lib/release-channel.js +1 -1
- package/lib/skill-registry.js +3 -3
- package/lib/workspace-events.js +1 -1
- package/lib/workspace-gates.js +2 -2
- package/lib/workspace-intelligence.js +1 -1
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace.js +16 -17
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +2 -2
- package/scripts/flow-adaptive-learning.js +6 -6
- package/scripts/flow-api-index.js +2 -2
- package/scripts/flow-architect-pass.js +1 -1
- package/scripts/flow-ask.js +1 -1
- package/scripts/flow-assumption-detector.js +1 -1
- package/scripts/flow-audit-gates.js +38 -12
- package/scripts/flow-audit.js +4 -4
- package/scripts/flow-auto-context.js +3 -3
- package/scripts/flow-background.js +1 -1
- package/scripts/flow-best-of-n.js +7 -7
- package/scripts/flow-bridge.js +3 -3
- package/scripts/flow-bug.js +2 -2
- package/scripts/flow-bulk-loop.js +7 -7
- package/scripts/flow-cascade-completion.js +2 -2
- package/scripts/flow-cascade.js +1 -1
- package/scripts/flow-checkpoint.js +2 -2
- package/scripts/flow-clarifying-questions.js +2 -2
- package/scripts/flow-cli.js +2 -2
- package/scripts/flow-code-intelligence.js +4 -4
- package/scripts/flow-community-sync.js +6 -6
- package/scripts/flow-community.js +1 -1
- package/scripts/flow-completion-truth-gate.js +161 -5
- package/scripts/flow-complexity.js +1 -1
- package/scripts/flow-config-defaults.js +16 -4
- package/scripts/flow-config-interactive.js +2 -2
- package/scripts/flow-config-loader.js +1 -1
- package/scripts/flow-config-migrate.js +5 -6
- package/scripts/flow-consistency-check.js +5 -5
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/index.js +2 -2
- package/scripts/flow-context-compact/section-extractor.js +3 -3
- package/scripts/flow-context-compact/summary-tree.js +1 -1
- package/scripts/flow-context-estimator.js +1 -1
- package/scripts/flow-context-gatherer.js +6 -6
- package/scripts/flow-context-generator.js +6 -6
- package/scripts/flow-context-init.js +2 -2
- package/scripts/flow-context-manager.js +1 -1
- package/scripts/flow-context-manifest.js +1 -1
- package/scripts/flow-context-monitor.js +5 -5
- package/scripts/flow-context-orchestrator.js +2 -2
- package/scripts/flow-context-scoring.js +4 -4
- package/scripts/flow-contract-scan.js +1 -1
- package/scripts/flow-correct.js +3 -3
- package/scripts/flow-damage-control.js +2 -2
- package/scripts/flow-deploy-gate.js +2 -2
- package/scripts/flow-deploy-history.js +1 -1
- package/scripts/flow-diff.js +3 -3
- package/scripts/flow-done-gates.js +1 -1
- package/scripts/flow-done.js +7 -7
- package/scripts/flow-durable-session.js +1 -1
- package/scripts/flow-entropy-monitor.js +3 -3
- package/scripts/flow-epics.js +5 -5
- package/scripts/flow-error-recovery.js +4 -4
- package/scripts/flow-eval-judge.js +5 -5
- package/scripts/flow-eval.js +7 -7
- package/scripts/flow-export-scanner.js +5 -5
- package/scripts/flow-extraction-review.js +1 -1
- package/scripts/flow-failure-learning.js +9 -9
- package/scripts/flow-feature.js +5 -5
- package/scripts/flow-figma-confirm.js +1 -1
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-index.js +2 -2
- package/scripts/flow-figma-match.js +1 -1
- package/scripts/flow-figma-mcp-server.js +3 -3
- package/scripts/flow-figma-orchestrator.js +1 -1
- package/scripts/flow-figma-registry.js +2 -2
- package/scripts/flow-function-index.js +2 -2
- package/scripts/flow-gate-confidence.js +2 -2
- package/scripts/flow-gate-telemetry.js +1 -1
- package/scripts/flow-gitignore.js +1 -1
- package/scripts/flow-guided-edit.js +3 -3
- package/scripts/flow-health.js +95 -8
- package/scripts/flow-hooks.js +3 -3
- package/scripts/flow-hybrid-detect.js +2 -2
- package/scripts/flow-hybrid-interactive.js +1 -1
- package/scripts/flow-hybrid-test.js +1 -1
- package/scripts/flow-hypothesis-generator.js +4 -4
- package/scripts/flow-instruction-richness.js +11 -11
- package/scripts/flow-intent-bootstrap.js +1 -1
- package/scripts/flow-intent-framing.js +1 -1
- package/scripts/flow-item-link.js +2 -2
- package/scripts/flow-knowledge-router.js +7 -7
- package/scripts/flow-knowledge-sync.js +3 -3
- package/scripts/flow-learning-orchestrator.js +1 -1
- package/scripts/flow-links.js +2 -2
- package/scripts/flow-log-manager.js +2 -2
- package/scripts/flow-logic-adversary.js +5 -4
- package/scripts/flow-long-input-chunking.js +1 -1
- package/scripts/flow-long-input-cli.js +3 -3
- package/scripts/flow-long-input.js +18 -18
- package/scripts/flow-loop-retry-learning.js +2 -2
- package/scripts/flow-lsp.js +4 -4
- package/scripts/flow-mcp-docs.js +1 -1
- package/scripts/flow-memory-blocks.js +5 -5
- package/scripts/flow-memory-compactor.js +3 -3
- package/scripts/flow-memory-db.js +4 -4
- package/scripts/flow-memory-sync.js +3 -3
- package/scripts/flow-metrics.js +2 -2
- package/scripts/flow-migrate-igr.js +2 -2
- package/scripts/flow-migrate.js +2 -2
- package/scripts/flow-model-adapter.js +4 -4
- package/scripts/flow-model-caller.js +8 -8
- package/scripts/flow-model-config.js +5 -5
- package/scripts/flow-model-profile.js +7 -7
- package/scripts/flow-model-router.js +5 -5
- package/scripts/flow-model-types.js +3 -3
- package/scripts/flow-models.js +8 -8
- package/scripts/flow-morning.js +1 -1
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +2 -2
- package/scripts/flow-orchestrate-llm.js +4 -4
- package/scripts/flow-orchestrate-rollback.js +1 -1
- package/scripts/flow-orchestrate-state.js +6 -6
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate-validation.js +2 -2
- package/scripts/flow-orchestrate-validator.js +1 -1
- package/scripts/flow-orchestrate.js +25 -25
- package/scripts/flow-parallel.js +1 -1
- package/scripts/flow-pattern-enforcer.js +7 -7
- package/scripts/flow-pattern-extractor.js +3 -3
- package/scripts/flow-peer-review.js +8 -8
- package/scripts/flow-pending.js +1 -1
- package/scripts/flow-permissions.js +1 -1
- package/scripts/flow-phased-task.js +1 -1
- package/scripts/flow-plan.js +1 -1
- package/scripts/flow-prd-manager.js +2 -2
- package/scripts/flow-product-scanner.js +2 -2
- package/scripts/flow-progress-tracker.js +2 -2
- package/scripts/flow-progress.js +1 -1
- package/scripts/flow-project-analyzer.js +3 -3
- package/scripts/flow-prompt-capture.js +2 -2
- package/scripts/flow-prompt-composer.js +3 -3
- package/scripts/flow-prompt-template.js +4 -4
- package/scripts/flow-providers.js +31 -23
- package/scripts/flow-queue.js +1 -1
- package/scripts/flow-registry-manager.js +4 -4
- package/scripts/flow-regression.js +1 -1
- package/scripts/flow-response-parser.js +1 -1
- package/scripts/flow-resume.js +1 -1
- package/scripts/flow-review-passes/index.js +2 -2
- package/scripts/flow-review-passes/integration.js +3 -3
- package/scripts/flow-review-passes/logic.js +3 -3
- package/scripts/flow-review-passes/security.js +2 -2
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-review.js +11 -11
- package/scripts/flow-revision-tracker.js +2 -2
- package/scripts/flow-roadmap.js +2 -2
- package/scripts/flow-run-trace.js +1 -1
- package/scripts/flow-safety.js +3 -3
- package/scripts/flow-scanner-base.js +1 -1
- package/scripts/flow-scenario-engine.js +7 -7
- package/scripts/flow-schema-drift.js +4 -3
- package/scripts/flow-section-index.js +2 -2
- package/scripts/flow-section-resolver.js +4 -4
- package/scripts/flow-semantic-match.js +3 -3
- package/scripts/flow-session-end.js +56 -0
- package/scripts/flow-session-learning.js +2 -2
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skill-create.js +3 -3
- package/scripts/flow-skill-freshness.js +2 -2
- package/scripts/flow-skill-generator.js +6 -6
- package/scripts/flow-skill-learn.js +7 -7
- package/scripts/flow-skill-matcher.js +2 -2
- package/scripts/flow-solution-optimizer.js +1 -1
- package/scripts/flow-spec-generator.js +5 -5
- package/scripts/flow-spec-verifier.js +2 -2
- package/scripts/flow-stack-wizard.js +6 -6
- package/scripts/flow-standards-checker.js +8 -8
- package/scripts/flow-standards-gate.js +4 -4
- package/scripts/flow-standards-learner.js +2 -2
- package/scripts/flow-start.js +9 -9
- package/scripts/flow-stats-collector.js +2 -2
- package/scripts/flow-status.js +1 -1
- package/scripts/flow-step-changelog.js +3 -3
- package/scripts/flow-step-complexity.js +1 -1
- package/scripts/flow-step-coverage.js +3 -3
- package/scripts/flow-step-knowledge.js +2 -2
- package/scripts/flow-step-pr-tests.js +2 -2
- package/scripts/flow-step-regression.js +3 -3
- package/scripts/flow-step-review.js +5 -5
- package/scripts/flow-story.js +2 -2
- package/scripts/flow-strict-adherence.js +2 -2
- package/scripts/flow-structure-sensor.js +283 -0
- package/scripts/flow-sync-anonymizer.js +3 -3
- package/scripts/flow-task-checkpoint.js +2 -2
- package/scripts/flow-task-classifier.js +2 -2
- package/scripts/flow-task-completion-summary.js +1 -1
- package/scripts/flow-task-enforcer.js +5 -5
- package/scripts/flow-tech-debt.js +3 -3
- package/scripts/flow-template-extractor.js +3 -3
- package/scripts/flow-templates.js +1 -1
- package/scripts/flow-test-api.js +12 -12
- package/scripts/flow-test-discovery.js +9 -9
- package/scripts/flow-test-generate.js +5 -5
- package/scripts/flow-test-integrity.js +3 -3
- package/scripts/flow-test-ui.js +8 -8
- package/scripts/flow-testing-deps.js +4 -4
- package/scripts/flow-tiered-learning.js +3 -3
- package/scripts/flow-todowrite-sync.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-verification-profile.js +9 -9
- package/scripts/flow-verify.js +2 -2
- package/scripts/flow-version-check.js +2 -2
- package/scripts/flow-webmcp-generator.js +3 -3
- package/scripts/flow-wiring-verifier.js +13 -13
- package/scripts/flow-worker-question-classifier.js +256 -0
- package/scripts/flow-workflow-steps.js +3 -3
- package/scripts/flow-workflow.js +1 -1
- package/scripts/flow-worktree.js +1 -1
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/commit-log-gate.js +2 -2
- package/scripts/hooks/core/component-check.js +3 -3
- package/scripts/hooks/core/config-change.js +1 -1
- package/scripts/hooks/core/deploy-gate.js +2 -1
- package/scripts/hooks/core/git-safety-gate.js +1 -1
- package/scripts/hooks/core/instructions-loaded.js +1 -1
- package/scripts/hooks/core/loop-check.js +1 -1
- package/scripts/hooks/core/manager-boundary-gate.js +3 -2
- package/scripts/hooks/core/observation-capture.js +6 -6
- package/scripts/hooks/core/phase-gate.js +4 -4
- package/scripts/hooks/core/pre-compact.js +1 -1
- package/scripts/hooks/core/pre-tool-orchestrator.js +1 -1
- package/scripts/hooks/core/routing-gate.js +2 -84
- package/scripts/hooks/core/session-context.js +1 -1
- package/scripts/hooks/core/session-end.js +3 -3
- package/scripts/hooks/core/session-history.js +1 -1
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +2 -4
- package/scripts/hooks/core/task-completed.js +13 -7
- package/scripts/hooks/core/task-created.js +1 -1
- package/scripts/hooks/core/worktree-lifecycle.js +1 -1
- package/scripts/hooks/entry/claude-code/permission-denied.js +4 -2
- package/scripts/hooks/entry/claude-code/stop.js +60 -0
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +1 -1
- package/scripts/hooks/git/post-commit.js +1 -1
- package/scripts/postinstall.js +7 -7
- package/scripts/preuninstall.js +5 -5
- package/scripts/registries/component-registry.js +2 -2
- package/scripts/registries/contract-scanner.js +11 -11
- package/scripts/registries/schema-registry.js +5 -5
- package/scripts/registries/service-registry.js +9 -9
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Worker Question Classifier (v2.21.0+)
|
|
5
|
+
*
|
|
6
|
+
* AI-based semantic detector that runs at Stop-hook time in workspace worker
|
|
7
|
+
* mode and answers ONE question about the AI's final assistant message:
|
|
8
|
+
*
|
|
9
|
+
* "Does this message end by asking the user a question that expects
|
|
10
|
+
* a user response?"
|
|
11
|
+
*
|
|
12
|
+
* If YES + confidence >= threshold + worker mode → Stop hook blocks the turn
|
|
13
|
+
* with instructions to channel-dispatch `## QUESTION:` to the manager instead.
|
|
14
|
+
*
|
|
15
|
+
* Why AI, not regex: the hedging vocabulary is infinite ("let me know",
|
|
16
|
+
* "should I", "which option", "?", "thoughts?", "any preference?"). User
|
|
17
|
+
* explicitly requested AI logic over regex in the 2026-04-16 session.
|
|
18
|
+
*
|
|
19
|
+
* Design mirrors flow-conclusion-classifier.js / flow-correction-detector.js:
|
|
20
|
+
* - Uses existing flow-model-caller.js infrastructure (same plan tokens
|
|
21
|
+
* Claude Code already uses)
|
|
22
|
+
* - ANTHROPIC_API_KEY absent → returns `{ classified: false, reason:
|
|
23
|
+
* 'no-credentials' }` — Stop hook treats as no-op, does NOT block.
|
|
24
|
+
* This matches the established fail-open pattern.
|
|
25
|
+
* - Transcript parsing is defensive — missing, empty, or malformed
|
|
26
|
+
* transcript returns `{ classified: false, reason: <specific> }`.
|
|
27
|
+
* - JSON response from Haiku validated for shape + prototype-pollution.
|
|
28
|
+
* - Fail-open on model error — if the classifier breaks, legitimate
|
|
29
|
+
* stops are not affected. A silent-stall false-negative is recoverable;
|
|
30
|
+
* a false-positive block on every turn is not.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const fs = require('node:fs');
|
|
34
|
+
|
|
35
|
+
const DEFAULT_MIN_CONFIDENCE = 70;
|
|
36
|
+
const DEFAULT_MODEL = 'anthropic:claude-3-5-haiku-latest';
|
|
37
|
+
const MAX_MESSAGE_CHARS = 8000; // Classifier input cap
|
|
38
|
+
const MAX_TOKENS = 300; // Classifier output cap (tiny JSON)
|
|
39
|
+
const TEMPERATURE = 0.0; // Deterministic classification
|
|
40
|
+
|
|
41
|
+
// Shared prototype-pollution guard (same as flow-conclusion-classifier).
|
|
42
|
+
const { DANGEROUS_KEYS } = require('./flow-io');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract the final assistant message from a Claude Code transcript JSONL file.
|
|
46
|
+
*
|
|
47
|
+
* Claude Code writes one JSON object per line. Each object represents an event
|
|
48
|
+
* (user message, assistant message, tool call, tool result, etc.). We scan
|
|
49
|
+
* backward for the last event where the content resembles an assistant-authored
|
|
50
|
+
* text block (has `role: 'assistant'` OR `type: 'assistant'` shape).
|
|
51
|
+
*
|
|
52
|
+
* Defensive: any IO / parse error returns null. Caller treats null as no-op.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} transcriptPath - Absolute path to the JSONL transcript
|
|
55
|
+
* @returns {string|null} The text of the last assistant message, or null
|
|
56
|
+
*/
|
|
57
|
+
function extractLastAssistantMessage(transcriptPath) {
|
|
58
|
+
if (!transcriptPath || typeof transcriptPath !== 'string') return null;
|
|
59
|
+
let raw;
|
|
60
|
+
try {
|
|
61
|
+
raw = fs.readFileSync(transcriptPath, 'utf-8');
|
|
62
|
+
} catch (_err) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (!raw || raw.length === 0) return null;
|
|
66
|
+
|
|
67
|
+
// Scan from end for lines we can parse as assistant messages.
|
|
68
|
+
const lines = raw.split('\n');
|
|
69
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
70
|
+
const line = lines[i].trim();
|
|
71
|
+
if (!line) continue;
|
|
72
|
+
let entry;
|
|
73
|
+
try {
|
|
74
|
+
entry = JSON.parse(line);
|
|
75
|
+
} catch (_err) {
|
|
76
|
+
continue; // Skip unparseable lines — transcript may be mid-write.
|
|
77
|
+
}
|
|
78
|
+
const text = extractAssistantText(entry);
|
|
79
|
+
if (text) return text.slice(0, MAX_MESSAGE_CHARS);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pull assistant-authored text out of a single transcript entry. The transcript
|
|
86
|
+
* format has evolved across Claude Code versions — we accept any of:
|
|
87
|
+
* - { role: 'assistant', content: 'string' }
|
|
88
|
+
* - { role: 'assistant', content: [{ type: 'text', text: '...' }] }
|
|
89
|
+
* - { type: 'assistant', message: { content: [{ type: 'text', text: '...' }] } }
|
|
90
|
+
* - { type: 'assistant', text: '...' }
|
|
91
|
+
*/
|
|
92
|
+
function extractAssistantText(entry) {
|
|
93
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
94
|
+
|
|
95
|
+
// Direct role/type check.
|
|
96
|
+
const isAssistant =
|
|
97
|
+
entry.role === 'assistant' ||
|
|
98
|
+
entry.type === 'assistant' ||
|
|
99
|
+
(entry.message && entry.message.role === 'assistant');
|
|
100
|
+
if (!isAssistant) return null;
|
|
101
|
+
|
|
102
|
+
// Pull the content wherever it lives.
|
|
103
|
+
const content = entry.content ?? entry.message?.content ?? entry.text;
|
|
104
|
+
if (typeof content === 'string') return content;
|
|
105
|
+
if (Array.isArray(content)) {
|
|
106
|
+
// Collect text blocks only — skip tool_use / tool_result blocks.
|
|
107
|
+
const texts = content
|
|
108
|
+
.filter(b => b && typeof b === 'object' && (b.type === 'text' || typeof b.text === 'string'))
|
|
109
|
+
.map(b => String(b.text || ''))
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
if (texts.length > 0) return texts.join('\n').trim();
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the Haiku classifier prompt. Kept as a pure string for testability.
|
|
118
|
+
*
|
|
119
|
+
* Designed to minimize false positives:
|
|
120
|
+
* - Rhetorical questions ("Did the tests pass? Yes.") → NO
|
|
121
|
+
* - Narration with trailing "?" ("the question is how to proceed") → NO
|
|
122
|
+
* - Actual open-ended ask ("Should I use A or B?" with no answer given) → YES
|
|
123
|
+
*/
|
|
124
|
+
function buildClassifierPrompt(lastMessage) {
|
|
125
|
+
return `You classify whether an AI assistant's final message to a WORKSPACE WORKER session ends by asking the USER a question that expects a user response.
|
|
126
|
+
|
|
127
|
+
Context: in workspace mode, workers are autonomous and MUST NOT prompt the user directly — the user only sees the manager terminal, so direct questions stall silently. Workers that need user input MUST channel-dispatch "## QUESTION:" to the manager instead.
|
|
128
|
+
|
|
129
|
+
Your job: classify YES only when the worker's final message contains an OPEN question the worker is waiting on the user to answer. Classify NO for rhetorical questions that the worker answers itself, narrative descriptions, or questions that have an accompanying decision.
|
|
130
|
+
|
|
131
|
+
[MESSAGE_START]
|
|
132
|
+
${String(lastMessage || '').slice(0, MAX_MESSAGE_CHARS)}
|
|
133
|
+
[MESSAGE_END]
|
|
134
|
+
|
|
135
|
+
Return JSON only, no prose, no markdown fences:
|
|
136
|
+
{"isUserQuestion": true|false, "confidence": 0-100, "reason": "one short sentence"}
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
- "Should I proceed with A or B? Let me know." → {"isUserQuestion": true, "confidence": 95, "reason": "open choice awaiting user decision"}
|
|
140
|
+
- "Did the tests pass? Yes, all 12 passed." → {"isUserQuestion": false, "confidence": 90, "reason": "rhetorical, answered inline"}
|
|
141
|
+
- "I finished the task. Awaiting your signal." → {"isUserQuestion": true, "confidence": 85, "reason": "hedging terminal state awaiting user"}
|
|
142
|
+
- "Task complete. Next: wf-abc12345 — starting now." → {"isUserQuestion": false, "confidence": 95, "reason": "action statement, no question"}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Guard parsed JSON response against prototype pollution. Mirrors
|
|
147
|
+
* flow-conclusion-classifier.hasDangerousKeys.
|
|
148
|
+
*/
|
|
149
|
+
function hasDangerousKeys(value) {
|
|
150
|
+
if (!value || typeof value !== 'object') return false;
|
|
151
|
+
if (Array.isArray(value)) return value.some(hasDangerousKeys);
|
|
152
|
+
for (const key of Object.keys(value)) {
|
|
153
|
+
if (DANGEROUS_KEYS.has(key)) return true;
|
|
154
|
+
if (hasDangerousKeys(value[key])) return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Classify the worker's last assistant message.
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} opts
|
|
163
|
+
* @param {string} opts.transcriptPath - Absolute path to session JSONL transcript
|
|
164
|
+
* @param {number} [opts.minConfidence] - Confidence threshold (default 70)
|
|
165
|
+
* @param {string} [opts.model] - Model override (default haiku)
|
|
166
|
+
* @returns {Promise<{
|
|
167
|
+
* classified: boolean,
|
|
168
|
+
* isUserQuestion?: boolean,
|
|
169
|
+
* confidence?: number,
|
|
170
|
+
* reason?: string,
|
|
171
|
+
* lastMessage?: string
|
|
172
|
+
* }>}
|
|
173
|
+
*/
|
|
174
|
+
async function classifyWorkerQuestion(opts = {}) {
|
|
175
|
+
const minConfidence = Number.isFinite(opts.minConfidence) ? opts.minConfidence : DEFAULT_MIN_CONFIDENCE;
|
|
176
|
+
const model = opts.model || DEFAULT_MODEL;
|
|
177
|
+
|
|
178
|
+
// Fail-open gates — any reason to skip returns { classified: false }.
|
|
179
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
180
|
+
return { classified: false, reason: 'no-credentials' };
|
|
181
|
+
}
|
|
182
|
+
if (!opts.transcriptPath) {
|
|
183
|
+
return { classified: false, reason: 'no-transcript-path' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const lastMessage = extractLastAssistantMessage(opts.transcriptPath);
|
|
187
|
+
if (!lastMessage || lastMessage.length < 10) {
|
|
188
|
+
return { classified: false, reason: 'no-last-message', lastMessage };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let callModel;
|
|
192
|
+
try {
|
|
193
|
+
({ callModel } = require('./flow-model-caller'));
|
|
194
|
+
} catch (_err) {
|
|
195
|
+
return { classified: false, reason: 'no-model-caller' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let result;
|
|
199
|
+
try {
|
|
200
|
+
result = await callModel(model, buildClassifierPrompt(lastMessage), {
|
|
201
|
+
temperature: TEMPERATURE,
|
|
202
|
+
maxTokens: MAX_TOKENS
|
|
203
|
+
});
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (process.env.DEBUG) {
|
|
206
|
+
console.error(`[worker-question-classifier] model call failed: ${err.message}`);
|
|
207
|
+
}
|
|
208
|
+
return { classified: false, reason: 'model-error' };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// flow-model-caller returns { success, response, ... } where `response` is the text.
|
|
212
|
+
// Earlier classifiers read `.content`; accept both shapes for resilience.
|
|
213
|
+
const raw = String(result?.response ?? result?.content ?? '').trim();
|
|
214
|
+
if (!raw) return { classified: false, reason: 'empty-response' };
|
|
215
|
+
|
|
216
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
217
|
+
if (!jsonMatch) return { classified: false, reason: 'non-json-response' };
|
|
218
|
+
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
222
|
+
} catch (_err) {
|
|
223
|
+
return { classified: false, reason: 'json-parse-error' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
227
|
+
return { classified: false, reason: 'bad-shape' };
|
|
228
|
+
}
|
|
229
|
+
if (hasDangerousKeys(parsed)) {
|
|
230
|
+
return { classified: false, reason: 'dangerous-keys' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const isUserQuestion = Boolean(parsed.isUserQuestion);
|
|
234
|
+
const confidence = Number.isFinite(parsed.confidence) ? Math.round(parsed.confidence) : 0;
|
|
235
|
+
const reason = typeof parsed.reason === 'string' ? parsed.reason.slice(0, 240) : '';
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
classified: true,
|
|
239
|
+
isUserQuestion,
|
|
240
|
+
confidence,
|
|
241
|
+
reason,
|
|
242
|
+
lastMessage,
|
|
243
|
+
blocked: isUserQuestion && confidence >= minConfidence,
|
|
244
|
+
minConfidence
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = {
|
|
249
|
+
classifyWorkerQuestion,
|
|
250
|
+
extractLastAssistantMessage,
|
|
251
|
+
extractAssistantText,
|
|
252
|
+
buildClassifierPrompt,
|
|
253
|
+
hasDangerousKeys,
|
|
254
|
+
DEFAULT_MIN_CONFIDENCE,
|
|
255
|
+
DEFAULT_MODEL
|
|
256
|
+
};
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* await runSteps('afterTask', { taskId: 'TASK-001', files: [...] });
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const {
|
|
14
|
+
const _fs = require('node:fs');
|
|
15
|
+
const _path = require('node:path');
|
|
16
|
+
const { colors, getConfig, invalidateConfigCache, writeJson, PATHS } = require('./flow-utils');
|
|
17
17
|
|
|
18
18
|
// ============================================================
|
|
19
19
|
// Step Registry - All available workflow steps
|
package/scripts/flow-workflow.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
const fs = require('node:fs');
|
|
25
25
|
const path = require('node:path');
|
|
26
26
|
const { spawn } = require('node:child_process');
|
|
27
|
-
const {
|
|
27
|
+
const { colors: c, readJson, PATHS } = require('./flow-utils');
|
|
28
28
|
const { success: printSuccess, error: printError } = require('./flow-output');
|
|
29
29
|
const { detectPackageManager } = require('./flow-script-resolver');
|
|
30
30
|
|
package/scripts/flow-worktree.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* await discardWorktree(worktree);
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
const { execFileSync
|
|
25
|
+
const { execFileSync } = require('node:child_process');
|
|
26
26
|
const fs = require('node:fs');
|
|
27
27
|
const path = require('node:path');
|
|
28
28
|
const os = require('node:os');
|
|
@@ -38,7 +38,7 @@ class BaseAdapter {
|
|
|
38
38
|
* @param {Object} coreResult - Result from core module
|
|
39
39
|
* @returns {Object} CLI-specific format
|
|
40
40
|
*/
|
|
41
|
-
transformResult(
|
|
41
|
+
transformResult(_event, coreResult) {
|
|
42
42
|
throw new Error('transformResult() must be implemented by subclass');
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -58,7 +58,7 @@ class BaseAdapter {
|
|
|
58
58
|
* @param {Object} [transportConfig] - Transport config: { transport, url, headers, allowedEnvVars }
|
|
59
59
|
* @returns {Object} CLI-specific hook configuration
|
|
60
60
|
*/
|
|
61
|
-
generateConfig(
|
|
61
|
+
generateConfig(_rules, projectRoot, transportConfig) {
|
|
62
62
|
throw new Error('generateConfig() must be implemented by subclass');
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -80,7 +80,7 @@ function checkCommitLogGate(command, config) {
|
|
|
80
80
|
|
|
81
81
|
// Load config if not provided
|
|
82
82
|
if (!config) {
|
|
83
|
-
try { config = getConfig(); } catch (
|
|
83
|
+
try { config = getConfig(); } catch (_err) { config = {}; }
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// Check if gate is enabled (default: enabled when enforcement section exists)
|
|
@@ -92,7 +92,7 @@ function checkCommitLogGate(command, config) {
|
|
|
92
92
|
let readyData;
|
|
93
93
|
try {
|
|
94
94
|
readyData = getReadyData();
|
|
95
|
-
} catch (
|
|
95
|
+
} catch (_err) {
|
|
96
96
|
// Can't read ready.json → fail-open (don't block work)
|
|
97
97
|
return { allowed: true, blocked: false };
|
|
98
98
|
}
|
|
@@ -191,7 +191,7 @@ function loadComponentIndex() {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
-
} catch (
|
|
194
|
+
} catch (_err) {
|
|
195
195
|
// Skip unreadable map files
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -199,7 +199,7 @@ function loadComponentIndex() {
|
|
|
199
199
|
|
|
200
200
|
if (components.length === 0) return null;
|
|
201
201
|
return { components, source: 'fallback-from-maps' };
|
|
202
|
-
} catch (
|
|
202
|
+
} catch (_err) {
|
|
203
203
|
return null;
|
|
204
204
|
}
|
|
205
205
|
}
|
|
@@ -238,7 +238,7 @@ function parseAppMap() {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
return components;
|
|
241
|
-
} catch (
|
|
241
|
+
} catch (_err) {
|
|
242
242
|
return [];
|
|
243
243
|
}
|
|
244
244
|
}
|
|
@@ -65,7 +65,7 @@ function handleConfigChange(options = {}) {
|
|
|
65
65
|
let bridgeState = null;
|
|
66
66
|
try {
|
|
67
67
|
bridgeState = require('../../flow-bridge-state');
|
|
68
|
-
const {
|
|
68
|
+
const { } = require('../../flow-utils');
|
|
69
69
|
} catch (_err) {
|
|
70
70
|
// Bridge state module unavailable
|
|
71
71
|
}
|
|
@@ -264,7 +264,8 @@ function findLatestArtifact(options) {
|
|
|
264
264
|
for (const file of files) {
|
|
265
265
|
const filePath = path.join(VERIFICATION_DIR, file);
|
|
266
266
|
try {
|
|
267
|
-
const artifact =
|
|
267
|
+
const artifact = safeJsonParse(filePath, null);
|
|
268
|
+
if (!artifact) continue;
|
|
268
269
|
|
|
269
270
|
// Verify HMAC signature
|
|
270
271
|
const sigResult = verifyArtifactSignature(artifact);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
const fs = require('node:fs');
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
|
|
17
|
-
const { safeJsonParse
|
|
17
|
+
const { safeJsonParse } = require('../../flow-utils');
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Check if new packages have been added since last scan.
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
const path = require('node:path');
|
|
29
29
|
const fs = require('node:fs');
|
|
30
|
+
const { safeJsonParse } = require('../../flow-io');
|
|
30
31
|
|
|
31
32
|
// ============================================================
|
|
32
33
|
// Member Path Resolution
|
|
@@ -50,7 +51,7 @@ function getMemberPaths() {
|
|
|
50
51
|
if (!fs.existsSync(manifestPath)) return [];
|
|
51
52
|
|
|
52
53
|
try {
|
|
53
|
-
const manifest =
|
|
54
|
+
const manifest = safeJsonParse(manifestPath, {});
|
|
54
55
|
const members = manifest.members || {};
|
|
55
56
|
const paths = [];
|
|
56
57
|
|
|
@@ -141,7 +142,7 @@ function getMemberPort(memberName) {
|
|
|
141
142
|
|
|
142
143
|
try {
|
|
143
144
|
const manifestPath = path.join(workspaceRoot, '.workspace', 'state', 'workspace-manifest.json');
|
|
144
|
-
const manifest =
|
|
145
|
+
const manifest = safeJsonParse(manifestPath, {});
|
|
145
146
|
const member = manifest.members?.[memberName];
|
|
146
147
|
return member?.port ?? member?.channelPort ?? null;
|
|
147
148
|
} catch (_err) {
|
|
@@ -161,7 +161,7 @@ function summarizeInput(toolName, toolInput) {
|
|
|
161
161
|
const inputStr = JSON.stringify(toolInput);
|
|
162
162
|
return `${toolName}: ${inputStr.slice(0, 60)}${inputStr.length > 60 ? '...' : ''}`;
|
|
163
163
|
}
|
|
164
|
-
} catch (
|
|
164
|
+
} catch (_err) {
|
|
165
165
|
return `${toolName}: (summarization failed)`;
|
|
166
166
|
}
|
|
167
167
|
}
|
|
@@ -232,7 +232,7 @@ function summarizeOutput(toolName, toolResponse, success) {
|
|
|
232
232
|
default:
|
|
233
233
|
return `Completed: ${responseStr.slice(0, 60)}${responseStr.length > 60 ? '...' : ''}`;
|
|
234
234
|
}
|
|
235
|
-
} catch (
|
|
235
|
+
} catch (_err) {
|
|
236
236
|
return 'Completed (summarization failed)';
|
|
237
237
|
}
|
|
238
238
|
}
|
|
@@ -298,7 +298,7 @@ async function captureObservation(options) {
|
|
|
298
298
|
const blocks = getMemoryBlocks();
|
|
299
299
|
const currentTask = blocks.getCurrentTask();
|
|
300
300
|
contextTaskId = currentTask?.id || null;
|
|
301
|
-
} catch (
|
|
301
|
+
} catch (_err) {
|
|
302
302
|
// Ignore - task context is optional
|
|
303
303
|
}
|
|
304
304
|
|
|
@@ -316,7 +316,7 @@ async function captureObservation(options) {
|
|
|
316
316
|
if (fullInput.length > maxInputSize) {
|
|
317
317
|
fullInput = fullInput.slice(0, maxInputSize) + '...[truncated]';
|
|
318
318
|
}
|
|
319
|
-
} catch (
|
|
319
|
+
} catch (_err) {
|
|
320
320
|
fullInput = '[serialization failed]';
|
|
321
321
|
}
|
|
322
322
|
|
|
@@ -328,7 +328,7 @@ async function captureObservation(options) {
|
|
|
328
328
|
if (fullOutput.length > maxOutputSize) {
|
|
329
329
|
fullOutput = fullOutput.slice(0, maxOutputSize) + '...[truncated]';
|
|
330
330
|
}
|
|
331
|
-
} catch (
|
|
331
|
+
} catch (_err) {
|
|
332
332
|
fullOutput = '[serialization failed]';
|
|
333
333
|
}
|
|
334
334
|
|
|
@@ -356,7 +356,7 @@ async function captureObservation(options) {
|
|
|
356
356
|
inputSummary || `${toolName} call`,
|
|
357
357
|
outputSummary || 'unknown error'
|
|
358
358
|
).catch(() => { /* non-blocking */ });
|
|
359
|
-
} catch (
|
|
359
|
+
} catch (_err) {
|
|
360
360
|
// Non-critical - memory pipeline may not be available
|
|
361
361
|
}
|
|
362
362
|
}
|
|
@@ -82,7 +82,7 @@ function isPhaseGateEnabled(config) {
|
|
|
82
82
|
try {
|
|
83
83
|
if (!config) config = getConfig();
|
|
84
84
|
return config.hooks?.rules?.phaseGate?.enabled === true;
|
|
85
|
-
} catch (
|
|
85
|
+
} catch (_err) {
|
|
86
86
|
return false; // Fail-open
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -104,7 +104,7 @@ function getCurrentPhase() {
|
|
|
104
104
|
updatedAt: data.updatedAt || null,
|
|
105
105
|
previousPhase: data.previousPhase || null
|
|
106
106
|
};
|
|
107
|
-
} catch (
|
|
107
|
+
} catch (_err) {
|
|
108
108
|
return defaults;
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -287,7 +287,7 @@ function checkPhaseGate(toolName, toolInput, config) {
|
|
|
287
287
|
/**
|
|
288
288
|
* Get guidance text for a blocked tool in a phase
|
|
289
289
|
*/
|
|
290
|
-
function getPhaseGuidance(phase,
|
|
290
|
+
function getPhaseGuidance(phase, _toolName) {
|
|
291
291
|
const guidance = {
|
|
292
292
|
idle: 'Route your request through a /wogi-* command first.',
|
|
293
293
|
routing: 'Wait for /wogi-start to finish routing.',
|
|
@@ -381,7 +381,7 @@ function checkAndResetStalePhase() {
|
|
|
381
381
|
return true;
|
|
382
382
|
}
|
|
383
383
|
return false;
|
|
384
|
-
} catch (
|
|
384
|
+
} catch (_err) {
|
|
385
385
|
return false;
|
|
386
386
|
}
|
|
387
387
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
const path = require('node:path');
|
|
23
23
|
const fs = require('node:fs');
|
|
24
|
-
const { PATHS, safeJsonParse
|
|
24
|
+
const { PATHS, safeJsonParse } = require('../../flow-utils');
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Phases where compaction should be BLOCKED because interruption
|
|
@@ -177,7 +177,7 @@ function runPreToolGates(ctx, deps) {
|
|
|
177
177
|
]);
|
|
178
178
|
if (!skipRoutingGateForSubagent && !skipRoutingGateForReadOnlyGit && GATED_TOOLS.has(toolName)) {
|
|
179
179
|
try {
|
|
180
|
-
const routingResult = deps.checkRoutingGate(toolName, config
|
|
180
|
+
const routingResult = deps.checkRoutingGate(toolName, config);
|
|
181
181
|
if (routingResult.blocked) {
|
|
182
182
|
return {
|
|
183
183
|
allowed: false,
|
|
@@ -250,7 +250,7 @@ function isRoutingPending() {
|
|
|
250
250
|
const age = Date.now() - new Date(data.timestamp).getTime();
|
|
251
251
|
if (age > ROUTING_FLAG_TTL_MS) {
|
|
252
252
|
// Flag is stale — clean it up and return false
|
|
253
|
-
try { fs.unlinkSync(ROUTING_FLAG_PATH); } catch (
|
|
253
|
+
try { fs.unlinkSync(ROUTING_FLAG_PATH); } catch (_err) { /* ignore cleanup failure */ }
|
|
254
254
|
if (process.env.DEBUG) {
|
|
255
255
|
console.error(`[routing-gate] Cleaned stale flag (${Math.round(age / 1000)}s old)`);
|
|
256
256
|
}
|
|
@@ -285,10 +285,9 @@ function isRoutingPending() {
|
|
|
285
285
|
*
|
|
286
286
|
* @param {string} toolName - The tool being called (e.g., 'Bash')
|
|
287
287
|
* @param {Object} [config] - Pre-loaded config (optional, falls back to getConfig())
|
|
288
|
-
* @param {Object} [toolInput] - Tool input (optional, used for v2.20.0 diagnostic bypass)
|
|
289
288
|
* @returns {{ allowed: boolean, blocked: boolean, reason: string, message: string|null }}
|
|
290
289
|
*/
|
|
291
|
-
function checkRoutingGate(toolName, config
|
|
290
|
+
function checkRoutingGate(toolName, config) {
|
|
292
291
|
// Gate ALL tools that allow the AI to act without routing through /wogi-start.
|
|
293
292
|
// Edit/Write/NotebookEdit were the critical gap: AI could edit ready.json (exempt
|
|
294
293
|
// from task gate) to create a fake active task, then edit anything freely.
|
|
@@ -318,26 +317,6 @@ function checkRoutingGate(toolName, config, toolInput) {
|
|
|
318
317
|
// This meant any in-progress task from a prior turn bypassed routing entirely.
|
|
319
318
|
// The only way to clear routing-pending is to invoke a /wogi-* skill.
|
|
320
319
|
|
|
321
|
-
// Gap D (v2.20.0) — diagnostic curl bypass for workspace workers.
|
|
322
|
-
// When a manager sends an INTROSPECTION/DIAGNOSTIC channel message, the
|
|
323
|
-
// worker needs to curl-reply to localhost:8800 with a structured "## " body.
|
|
324
|
-
// Without this bypass, answering diagnostic questions forces the worker to
|
|
325
|
-
// create a fake task just to satisfy routing — which is itself an
|
|
326
|
-
// anti-pattern. Narrow allowlist: Bash + curl + localhost:manager-port +
|
|
327
|
-
// body starts with "## " + config flag enabled.
|
|
328
|
-
try {
|
|
329
|
-
if (toolName === 'Bash' && isDiagnosticCurlBypass(toolInput, config)) {
|
|
330
|
-
return {
|
|
331
|
-
allowed: true,
|
|
332
|
-
blocked: false,
|
|
333
|
-
reason: 'diagnostic_curl_bypass',
|
|
334
|
-
message: null
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
} catch (_err) {
|
|
338
|
-
// Fail-closed — if bypass check errors, default to the normal block path.
|
|
339
|
-
}
|
|
340
|
-
|
|
341
320
|
// Block: routing is pending and no /wogi-* command has been invoked this turn
|
|
342
321
|
// NOTE: This message is shown to the AI as permissionDecisionReason.
|
|
343
322
|
// It must be prescriptive enough that the AI invokes /wogi-start instead of
|
|
@@ -358,66 +337,6 @@ function checkRoutingGate(toolName, config, toolInput) {
|
|
|
358
337
|
};
|
|
359
338
|
}
|
|
360
339
|
|
|
361
|
-
/**
|
|
362
|
-
* Gap D — recognize a narrow curl-to-manager bypass for diagnostic replies.
|
|
363
|
-
*
|
|
364
|
-
* Allowed iff ALL hold:
|
|
365
|
-
* - config.workspace.diagnosticCurlBypass !== false
|
|
366
|
-
* - Tool is Bash and command contains a single curl to
|
|
367
|
-
* http(s)://(127\\.0\\.0\\.1|localhost):{managerPort} (default 8800)
|
|
368
|
-
* - The curl body (`-d`, `--data`, `--data-binary`, `--data-raw`) starts
|
|
369
|
-
* with "## " (structured channel reply marker)
|
|
370
|
-
* - Body contains one of the diagnostic markers: "INTROSPECTION",
|
|
371
|
-
* "DIAGNOSTIC", "## QUESTION:", or "## ANSWER:" (so generic curl-to-8800
|
|
372
|
-
* doesn't escape routing — only diagnostic/question/answer replies do)
|
|
373
|
-
*
|
|
374
|
-
* This bypass is specifically NARROW by design — we want to unblock diagnostic
|
|
375
|
-
* round-trips without opening a back door. Generic curl to any URL, curl to a
|
|
376
|
-
* different port, or curl with a non-"## " body all still hit the normal block.
|
|
377
|
-
*
|
|
378
|
-
* @param {Object} toolInput - Bash tool input ({ command: string, ... })
|
|
379
|
-
* @param {Object} config - Loaded config
|
|
380
|
-
* @returns {boolean}
|
|
381
|
-
*/
|
|
382
|
-
function isDiagnosticCurlBypass(toolInput, config) {
|
|
383
|
-
if (!toolInput || typeof toolInput !== 'object') return false;
|
|
384
|
-
if (config?.workspace?.diagnosticCurlBypass === false) return false;
|
|
385
|
-
|
|
386
|
-
const command = String(toolInput.command || '');
|
|
387
|
-
if (!command.includes('curl')) return false;
|
|
388
|
-
|
|
389
|
-
// Must target localhost or 127.0.0.1 on the manager port.
|
|
390
|
-
const managerPort = process.env.WOGI_MANAGER_PORT ||
|
|
391
|
-
String(config?.workspace?.managerPort || '8800');
|
|
392
|
-
// Validate port shape first — prevents regex injection.
|
|
393
|
-
if (!/^\d{2,5}$/.test(String(managerPort))) return false;
|
|
394
|
-
const portPattern = new RegExp(
|
|
395
|
-
`https?://(?:127\\.0\\.0\\.1|localhost):${managerPort}(?:[/\\s"'\\\\]|$)`
|
|
396
|
-
);
|
|
397
|
-
if (!portPattern.test(command)) return false;
|
|
398
|
-
|
|
399
|
-
// Extract the body argument. Recognized flags: -d, --data, --data-binary,
|
|
400
|
-
// --data-raw, --data-urlencode. The body can be:
|
|
401
|
-
// (a) literal string: -d "## ANSWER: ..."
|
|
402
|
-
// (b) @- (from stdin — we can't inspect)
|
|
403
|
-
// (c) @filename
|
|
404
|
-
const bodyMatch = command.match(
|
|
405
|
-
/--data(?:-binary|-raw|-urlencode)?\s+(['"])([\s\S]*?)\1|-d\s+(['"])([\s\S]*?)\3/
|
|
406
|
-
);
|
|
407
|
-
const literalBody = bodyMatch ? (bodyMatch[2] || bodyMatch[4] || '') : '';
|
|
408
|
-
|
|
409
|
-
// Stdin / file bodies (@-) cannot be inspected — we conservatively reject
|
|
410
|
-
// them for this bypass. The worker should use literal `-d "## ..."` instead.
|
|
411
|
-
if (/--data(?:-binary|-raw|-urlencode)?\s+@|-d\s+@/.test(command) && !literalBody) {
|
|
412
|
-
return false;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (!literalBody.startsWith('## ')) return false;
|
|
416
|
-
|
|
417
|
-
// Final marker check — body must contain one of the diagnostic markers.
|
|
418
|
-
const markers = ['INTROSPECTION', 'DIAGNOSTIC', '## QUESTION:', '## ANSWER:'];
|
|
419
|
-
return markers.some(m => literalBody.includes(m));
|
|
420
|
-
}
|
|
421
340
|
|
|
422
341
|
/**
|
|
423
342
|
* Increment the stop-attempt counter in the routing flag.
|
|
@@ -467,7 +386,6 @@ function incrementStopAttempts(maxAttempts = 10) {
|
|
|
467
386
|
}
|
|
468
387
|
|
|
469
388
|
module.exports = {
|
|
470
|
-
isDiagnosticCurlBypass,
|
|
471
389
|
isRoutingGateEnabled,
|
|
472
390
|
hasActiveTask,
|
|
473
391
|
setRoutingPending,
|
|
@@ -809,7 +809,7 @@ async function gatherSessionContext(options = {}) {
|
|
|
809
809
|
console.error(`[session-context] Community sync-down failed: ${err.message}`);
|
|
810
810
|
}
|
|
811
811
|
});
|
|
812
|
-
} catch (
|
|
812
|
+
} catch (_err) {
|
|
813
813
|
// Non-critical — community sync module may not be available
|
|
814
814
|
}
|
|
815
815
|
|